ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/Deliantra-Client/DC/UI.pm
Revision: 1.384
Committed: Fri Jul 20 16:32:11 2007 UTC (16 years, 10 months ago) by root
Branch: MAIN
Changes since 1.383: +2376 -0 lines
Log Message:
change Table->add to add_at method and deprecate add method (to be replaced by a container-compliant one)

File Contents

# User Rev Content
1 root 1.340 package CFPlus::UI;
2 root 1.8
3 root 1.194 use utf8;
4 elmex 1.1 use strict;
5 root 1.18
6 root 1.74 use List::Util ();
7 root 1.284 use Event;
8 root 1.18
9 root 1.340 use CFPlus;
10 root 1.341 use CFPlus::Pod;
11 root 1.340 use CFPlus::Texture;
12 root 1.41
13 root 1.51 our ($FOCUS, $HOVER, $GRAB); # various widgets
14    
15 elmex 1.242 our $LAYOUT;
16 root 1.113 our $ROOT;
17 root 1.151 our $TOOLTIP;
18 root 1.51 our $BUTTON_STATE;
19    
20 root 1.202 our %WIDGET; # all widgets, weak-referenced
21    
22 root 1.284 our $TOOLTIP_WATCHER = Event->idle (min => 1/60, cb => sub {
23 root 1.151 if (!$GRAB) {
24     for (my $widget = $HOVER; $widget; $widget = $widget->{parent}) {
25 root 1.160 if (length $widget->{tooltip}) {
26 root 1.151 if ($TOOLTIP->{owner} != $widget) {
27 root 1.366 $TOOLTIP->{owner}->emit ("tooltip_hide") if $TOOLTIP->{owner};
28 root 1.253 $TOOLTIP->hide;
29    
30 root 1.151 $TOOLTIP->{owner} = $widget;
31 root 1.366 $TOOLTIP->{owner}->emit ("tooltip_show") if $TOOLTIP->{owner};
32 root 1.155
33 root 1.285 return if $ENV{CFPLUS_DEBUG} & 8;
34    
35 root 1.155 my $tip = $widget->{tooltip};
36    
37     $tip = $tip->($widget) if CODE:: eq ref $tip;
38    
39 root 1.196 $TOOLTIP->set_tooltip_from ($widget);
40 root 1.252 $TOOLTIP->show;
41 root 1.151 }
42    
43     return;
44     }
45     }
46     }
47    
48     $TOOLTIP->hide;
49 root 1.366 $TOOLTIP->{owner}->emit ("tooltip_hide") if $TOOLTIP->{owner};
50 root 1.151 delete $TOOLTIP->{owner};
51 root 1.284 });
52    
53     sub get_layout {
54     my $layout;
55    
56     for (grep { $_->{name} } values %WIDGET) {
57     my $win = $layout->{$_->{name}} = { };
58    
59     $win->{x} = ($_->{x} + $_->{w} * 0.5) / $::WIDTH if $_->{x} =~ /^[0-9.]+$/;
60     $win->{y} = ($_->{y} + $_->{h} * 0.5) / $::HEIGHT if $_->{y} =~ /^[0-9.]+$/;
61     $win->{w} = $_->{w} / $::WIDTH if defined $_->{w};
62     $win->{h} = $_->{h} / $::HEIGHT if defined $_->{h};
63    
64     $win->{show} = $_->{visible} && $_->{is_toplevel};
65     }
66    
67     $layout
68     }
69    
70     sub set_layout {
71     my ($layout) = @_;
72    
73     $LAYOUT = $layout;
74 root 1.151 }
75    
76 elmex 1.1 # class methods for events
77 root 1.51 sub feed_sdl_key_down_event {
78 root 1.231 $FOCUS->emit (key_down => $_[0])
79 root 1.163 if $FOCUS;
80 root 1.51 }
81    
82     sub feed_sdl_key_up_event {
83 root 1.231 $FOCUS->emit (key_up => $_[0])
84 root 1.163 if $FOCUS;
85 root 1.51 }
86    
87 root 1.330 sub check_hover {
88     my ($widget) = @_;
89    
90     if ($widget != $HOVER) {
91     my $hover = $HOVER; $HOVER = $widget;
92    
93     $hover->update if $hover && $hover->{can_hover};
94     $HOVER->update if $HOVER && $HOVER->{can_hover};
95    
96     $TOOLTIP_WATCHER->start;
97     }
98     }
99    
100 root 1.51 sub feed_sdl_button_down_event {
101     my ($ev) = @_;
102 root 1.137 my ($x, $y) = ($ev->{x}, $ev->{y});
103 root 1.51
104 root 1.339 $BUTTON_STATE |= 1 << ($ev->{button} - 1);
105    
106     unless ($GRAB) {
107 root 1.113 my $widget = $ROOT->find_widget ($x, $y);
108 root 1.51
109     $GRAB = $widget;
110     $GRAB->update if $GRAB;
111 root 1.151
112 root 1.284 $TOOLTIP_WATCHER->cb->();
113 root 1.51 }
114    
115 root 1.330 if ($GRAB) {
116     if ($ev->{button} == 4 || $ev->{button} == 5) {
117     # mousewheel
118     $ev->{dx} = 0;
119     $ev->{dy} = $ev->{button} * 2 - 9;
120     $GRAB->emit (mouse_wheel => $ev);
121     } else {
122     $GRAB->emit (button_down => $ev)
123     }
124     }
125 root 1.51 }
126    
127     sub feed_sdl_button_up_event {
128     my ($ev) = @_;
129    
130 root 1.330 my $widget = $GRAB || $ROOT->find_widget ($ev->{x}, $ev->{y});
131 root 1.51
132 root 1.137 $BUTTON_STATE &= ~(1 << ($ev->{button} - 1));
133 root 1.51
134 root 1.330 $GRAB->emit (button_up => $ev)
135     if $GRAB && $ev->{button} != 4 && $ev->{button} != 5;
136 root 1.58
137 root 1.330 unless ($BUTTON_STATE) {
138 root 1.51 my $grab = $GRAB; undef $GRAB;
139     $grab->update if $grab;
140     $GRAB->update if $GRAB;
141 root 1.151
142 root 1.330 check_hover $widget;
143 root 1.284 $TOOLTIP_WATCHER->cb->();
144 root 1.51 }
145     }
146    
147     sub feed_sdl_motion_event {
148     my ($ev) = @_;
149 root 1.137 my ($x, $y) = ($ev->{x}, $ev->{y});
150 root 1.51
151 root 1.113 my $widget = $GRAB || $ROOT->find_widget ($x, $y);
152 root 1.51
153 root 1.330 check_hover $widget;
154 root 1.51
155 root 1.330 $HOVER->emit (mouse_motion => $ev)
156 root 1.231 if $HOVER;
157 root 1.51 }
158 elmex 1.1
159 root 1.112 # convert position array to integers
160     sub harmonize {
161     my ($vals) = @_;
162    
163     my $rem = 0;
164    
165 root 1.116 for (@$vals) {
166 root 1.112 my $i = int $_ + $rem;
167     $rem += $_ - $i;
168     $_ = $i;
169     }
170     }
171    
172 root 1.220 sub full_refresh {
173     # make a copy, otherwise for complains about freed values.
174     my @widgets = values %WIDGET;
175    
176     $_->update
177     for @widgets;
178     }
179    
180 root 1.230 sub reconfigure_widgets {
181     # make a copy, otherwise C<for> complains about freed values.
182     my @widgets = values %WIDGET;
183    
184     $_->reconfigure
185     for @widgets;
186     }
187    
188 root 1.202 # call when resolution changes etc.
189     sub rescale_widgets {
190     my ($sx, $sy) = @_;
191    
192 root 1.230 for my $widget (values %WIDGET) {
193     if ($widget->{is_toplevel}) {
194 root 1.268 $widget->{x} += int $widget->{w} * 0.5 if $widget->{x} =~ /^[0-9.]+$/;
195     $widget->{y} += int $widget->{h} * 0.5 if $widget->{y} =~ /^[0-9.]+$/;
196 root 1.256
197     $widget->{x} = int 0.5 + $widget->{x} * $sx if $widget->{x} =~ /^[0-9.]+$/;
198     $widget->{w} = int 0.5 + $widget->{w} * $sx if exists $widget->{w};
199     $widget->{force_w} = int 0.5 + $widget->{force_w} * $sx if exists $widget->{force_w};
200     $widget->{y} = int 0.5 + $widget->{y} * $sy if $widget->{y} =~ /^[0-9.]+$/;
201     $widget->{h} = int 0.5 + $widget->{h} * $sy if exists $widget->{h};
202     $widget->{force_h} = int 0.5 + $widget->{force_h} * $sy if exists $widget->{force_h};
203    
204 root 1.268 $widget->{x} -= int $widget->{w} * 0.5 if $widget->{x} =~ /^[0-9.]+$/;
205     $widget->{y} -= int $widget->{h} * 0.5 if $widget->{y} =~ /^[0-9.]+$/;
206 root 1.256
207 root 1.202 }
208 root 1.230 }
209 root 1.202
210 root 1.230 reconfigure_widgets;
211 root 1.202 }
212    
213 root 1.73 #############################################################################
214    
215 root 1.340 package CFPlus::UI::Event;
216 root 1.330
217     sub xy {
218     $_[1]->coord2local ($_[0]{x}, $_[0]{y})
219     }
220    
221     #############################################################################
222    
223 root 1.340 package CFPlus::UI::Base;
224 root 1.73
225     use strict;
226    
227 root 1.340 use CFPlus::OpenGL;
228 root 1.73
229 elmex 1.1 sub new {
230     my $class = shift;
231 root 1.10
232 root 1.79 my $self = bless {
233 root 1.256 x => "center",
234     y => "center",
235 root 1.164 z => 0,
236 root 1.256 w => undef,
237     h => undef,
238 elmex 1.150 can_events => 1,
239 root 1.65 @_
240 root 1.79 }, $class;
241    
242 root 1.362 CFPlus::weaken ($CFPlus::UI::WIDGET{$self+0} = $self);
243 root 1.258
244     for (keys %$self) {
245     if (/^on_(.*)$/) {
246     $self->connect ($1 => delete $self->{$_});
247     }
248     }
249    
250 root 1.340 if (my $layout = $CFPlus::UI::LAYOUT->{$self->{name}}) {
251     $self->{x} = $layout->{x} * $CFPlus::UI::ROOT->{alloc_w} if exists $layout->{x};
252     $self->{y} = $layout->{y} * $CFPlus::UI::ROOT->{alloc_h} if exists $layout->{y};
253     $self->{force_w} = $layout->{w} * $CFPlus::UI::ROOT->{alloc_w} if exists $layout->{w};
254     $self->{force_h} = $layout->{h} * $CFPlus::UI::ROOT->{alloc_h} if exists $layout->{h};
255 root 1.256
256     $self->{x} -= $self->{force_w} * 0.5 if exists $layout->{x};
257     $self->{y} -= $self->{force_h} * 0.5 if exists $layout->{y};
258    
259 root 1.258 $self->show if $layout->{show};
260 root 1.79 }
261    
262     $self
263 elmex 1.1 }
264    
265 root 1.159 sub destroy {
266     my ($self) = @_;
267    
268     $self->hide;
269 root 1.361 $self->emit ("destroy");
270 root 1.159 %$self = ();
271     }
272    
273 root 1.368 sub TO_JSON {
274     { __widget_ref__ => $_[0]{s_id} }
275     }
276    
277 root 1.134 sub show {
278     my ($self) = @_;
279 root 1.248
280 root 1.134 return if $self->{parent};
281    
282 root 1.340 $CFPlus::UI::ROOT->add ($self);
283 root 1.134 }
284    
285 root 1.246 sub set_visible {
286     my ($self) = @_;
287    
288     return if $self->{visible};
289    
290     $self->{root} = $self->{parent}{root};
291     $self->{visible} = $self->{parent}{visible} + 1;
292    
293 root 1.248 $self->emit (visibility_change => 1);
294 root 1.251
295 root 1.252 $self->realloc if !exists $self->{req_w};
296 root 1.251
297     $_->set_visible for $self->children;
298 root 1.246 }
299    
300 root 1.231 sub set_invisible {
301 root 1.134 my ($self) = @_;
302    
303 root 1.244 return unless $self->{visible};
304    
305 root 1.251 $_->set_invisible for $self->children;
306 root 1.231
307 root 1.302 delete $self->{visible};
308 root 1.241 delete $self->{root};
309 root 1.231
310 root 1.163 undef $GRAB if $GRAB == $self;
311     undef $HOVER if $HOVER == $self;
312 root 1.134
313 root 1.340 $CFPlus::UI::TOOLTIP_WATCHER->cb->()
314 root 1.251 if $TOOLTIP->{owner} == $self;
315 root 1.232
316 root 1.305 $self->emit ("focus_out");
317 root 1.244 $self->emit (visibility_change => 0);
318 root 1.231 }
319    
320 root 1.250 sub set_visibility {
321     my ($self, $visible) = @_;
322    
323     return if $self->{visible} == $visible;
324    
325 root 1.369 $visible ? $self->show
326     : $self->hide;
327 root 1.250 }
328    
329     sub toggle_visibility {
330     my ($self) = @_;
331    
332     $self->{visible}
333     ? $self->hide
334     : $self->show;
335     }
336    
337 root 1.231 sub hide {
338     my ($self) = @_;
339    
340     $self->set_invisible;
341    
342 root 1.163 $self->{parent}->remove ($self)
343     if $self->{parent};
344 root 1.134 }
345    
346 root 1.256 sub move_abs {
347 root 1.18 my ($self, $x, $y, $z) = @_;
348 root 1.128
349 root 1.296 $self->{x} = List::Util::max 0, List::Util::min $self->{root}{w} - $self->{w}, int $x;
350     $self->{y} = List::Util::max 0, List::Util::min $self->{root}{h} - $self->{h}, int $y;
351 root 1.18 $self->{z} = $z if defined $z;
352 root 1.128
353     $self->update;
354 root 1.18 }
355    
356 root 1.158 sub set_size {
357     my ($self, $w, $h) = @_;
358    
359 root 1.256 $self->{force_w} = $w;
360     $self->{force_h} = $h;
361 root 1.158
362 root 1.251 $self->realloc;
363 elmex 1.20 }
364    
365 root 1.14 sub size_request {
366 elmex 1.36 require Carp;
367 root 1.147 Carp::confess "size_request is abstract";
368 elmex 1.36 }
369    
370 root 1.311 sub baseline_shift {
371     0
372     }
373    
374 root 1.128 sub configure {
375 root 1.75 my ($self, $x, $y, $w, $h) = @_;
376    
377 root 1.141 if ($self->{aspect}) {
378 root 1.256 my ($ow, $oh) = ($w, $h);
379    
380 root 1.340 $w = List::Util::min $w, CFPlus::ceil $h * $self->{aspect};
381     $h = List::Util::min $h, CFPlus::ceil $w / $self->{aspect};
382 root 1.141
383     # use alignment to adjust x, y
384 root 1.75
385 root 1.256 $x += int 0.5 * ($ow - $w);
386     $y += int 0.5 * ($oh - $h);
387 root 1.140 }
388    
389 root 1.256 if ($self->{x} ne $x || $self->{y} ne $y) {
390 root 1.141 $self->{x} = $x;
391     $self->{y} = $y;
392     $self->update;
393     }
394 root 1.40
395 root 1.259 if ($self->{alloc_w} != $w || $self->{alloc_h} != $h) {
396 root 1.251 return unless $self->{visible};
397    
398 root 1.259 $self->{alloc_w} = $w;
399     $self->{alloc_h} = $h;
400    
401     $self->{root}{size_alloc}{$self+0} = $self;
402 root 1.141 }
403 root 1.75 }
404    
405 root 1.202 sub children {
406 root 1.272 # nop
407     }
408    
409     sub visible_children {
410     $_[0]->children
411 root 1.202 }
412    
413 root 1.157 sub set_max_size {
414     my ($self, $w, $h) = @_;
415    
416 root 1.281 $self->{max_w} = int $w if defined $w;
417     $self->{max_h} = int $h if defined $h;
418 root 1.280
419     $self->realloc;
420 root 1.157 }
421    
422 root 1.209 sub set_tooltip {
423     my ($self, $tooltip) = @_;
424    
425 root 1.234 $tooltip =~ s/^\s+//;
426     $tooltip =~ s/\s+$//;
427    
428     return if $self->{tooltip} eq $tooltip;
429    
430 root 1.209 $self->{tooltip} = $tooltip;
431    
432 root 1.340 if ($CFPlus::UI::TOOLTIP->{owner} == $self) {
433     delete $CFPlus::UI::TOOLTIP->{owner};
434     $CFPlus::UI::TOOLTIP_WATCHER->cb->();
435 root 1.209 }
436     }
437    
438 root 1.82 # translate global coordinates to local coordinate system
439 root 1.113 sub coord2local {
440 root 1.58 my ($self, $x, $y) = @_;
441    
442 root 1.331 Carp::confess unless $self->{parent};#d#
443    
444 root 1.191 $self->{parent}->coord2local ($x - $self->{x}, $y - $self->{y})
445 root 1.113 }
446    
447     # translate local coordinates to global coordinate system
448     sub coord2global {
449     my ($self, $x, $y) = @_;
450    
451 root 1.332 Carp::confess unless $self->{parent};#d#
452    
453 root 1.191 $self->{parent}->coord2global ($x + $self->{x}, $y + $self->{y})
454 root 1.58 }
455    
456 root 1.305 sub invoke_focus_in {
457 root 1.51 my ($self) = @_;
458    
459 root 1.68 return if $FOCUS == $self;
460 root 1.97 return unless $self->{can_focus};
461 root 1.68
462 root 1.317 $FOCUS = $self;
463 elmex 1.120
464 root 1.317 $self->update;
465 root 1.305
466     0
467 elmex 1.1 }
468 root 1.4
469 root 1.305 sub invoke_focus_out {
470 root 1.51 my ($self) = @_;
471 root 1.4
472 root 1.51 return unless $FOCUS == $self;
473 root 1.4
474 root 1.317 undef $FOCUS;
475 elmex 1.120
476 root 1.317 $self->update;
477 root 1.231
478 root 1.305 $::MAPWIDGET->grab_focus #d# focus mapwidget if no other widget has focus
479 root 1.231 unless $FOCUS;
480 root 1.305
481     0
482 elmex 1.1 }
483 root 1.4
484 root 1.305 sub grab_focus {
485     my ($self) = @_;
486    
487 root 1.317 $FOCUS->emit ("focus_out") if $FOCUS;
488 root 1.305 $self->emit ("focus_in");
489     }
490    
491 root 1.328 sub invoke_mouse_motion { 0 }
492     sub invoke_button_up { 0 }
493     sub invoke_key_down { 0 }
494     sub invoke_key_up { 0 }
495 root 1.330 sub invoke_mouse_wheel { 0 }
496 root 1.51
497 root 1.305 sub invoke_button_down {
498 root 1.68 my ($self, $ev, $x, $y) = @_;
499    
500 root 1.305 $self->grab_focus;
501    
502 root 1.328 0
503 root 1.305 }
504    
505     sub connect {
506     my ($self, $signal, $cb) = @_;
507    
508     push @{ $self->{signal_cb}{$signal} }, $cb;
509 root 1.332
510 root 1.340 defined wantarray and CFPlus::guard {
511 root 1.332 @{ $self->{signal_cb}{$signal} } = grep $_ != $cb,
512     @{ $self->{signal_cb}{$signal} };
513     }
514 root 1.305 }
515 root 1.271
516 root 1.361 sub disconnect_all {
517     my ($self, $signal) = @_;
518    
519     delete $self->{signal_cb}{$signal};
520     }
521    
522 root 1.331 my %has_coords = (
523     button_down => 1,
524     button_up => 1,
525     mouse_motion => 1,
526     mouse_wheel => 1,
527     );
528    
529 root 1.305 sub emit {
530     my ($self, $signal, @args) = @_;
531    
532 root 1.354 # I do not really like this solution, but I do not like duplication
533     # and needlessly verbose code, either.
534 root 1.330 my @append
535 root 1.331 = $has_coords{$signal}
536 root 1.330 ? $args[0]->xy ($self)
537     : ();
538    
539 root 1.329 #warn +(caller(1))[3] . "emit $signal on $self (parent $self->{parent})\n";#d#
540    
541 root 1.362 for my $cb (
542     @{$self->{signal_cb}{$signal} || []}, # before
543     ($self->can ("invoke_$signal") || sub { 1 }), # closure
544     ) {
545     return $cb->($self, @args, @append) || next;
546     }
547    
548     # parent
549     $self->{parent} && $self->{parent}->emit ($signal, @args)
550 root 1.68 }
551    
552 root 1.251 sub find_widget {
553     my ($self, $x, $y) = @_;
554    
555     return () unless $self->{can_events};
556    
557     return $self
558     if $x >= $self->{x} && $x < $self->{x} + $self->{w}
559     && $y >= $self->{y} && $y < $self->{y} + $self->{h};
560    
561     ()
562     }
563    
564     sub set_parent {
565     my ($self, $parent) = @_;
566    
567 root 1.362 CFPlus::weaken ($self->{parent} = $parent);
568 root 1.251 $self->set_visible if $parent->{visible};
569     }
570    
571     sub realloc {
572     my ($self) = @_;
573    
574 root 1.252 if ($self->{visible}) {
575 root 1.259 return if $self->{root}{realloc}{$self+0};
576 root 1.251
577 root 1.259 $self->{root}{realloc}{$self+0} = $self;
578 root 1.252 $self->{root}->update;
579     } else {
580     delete $self->{req_w};
581 root 1.259 delete $self->{req_h};
582 root 1.252 }
583 root 1.251 }
584    
585     sub update {
586     my ($self) = @_;
587    
588     $self->{parent}->update
589     if $self->{parent};
590     }
591    
592 root 1.256 sub reconfigure {
593     my ($self) = @_;
594    
595     $self->realloc;
596     $self->update;
597     }
598    
599 root 1.267 # using global variables seems a bit hacky, but passing through all drawing
600     # functions seems pointless.
601     our ($draw_x, $draw_y, $draw_w, $draw_h); # screen rectangle being drawn
602    
603 elmex 1.1 sub draw {
604 elmex 1.11 my ($self) = @_;
605    
606 root 1.68 return unless $self->{h} && $self->{w};
607    
608 root 1.268 # update screen rectangle
609 root 1.267 local $draw_x = $draw_x + $self->{x};
610     local $draw_y = $draw_y + $self->{y};
611    
612 root 1.268 # skip widgets that are entirely outside the drawing area
613     return if ($draw_x + $self->{w} < 0) || ($draw_x >= $draw_w)
614     || ($draw_y + $self->{h} < 0) || ($draw_y >= $draw_h);
615 root 1.267
616 elmex 1.11 glPushMatrix;
617 root 1.12 glTranslate $self->{x}, $self->{y}, 0;
618 root 1.72
619 root 1.97 if ($self == $HOVER && $self->{can_hover}) {
620 root 1.278 glColor 1*0.2, 0.8*0.2, 0.5*0.2, 0.2;
621 root 1.50 glEnable GL_BLEND;
622 root 1.278 glBlendFunc GL_ONE, GL_ONE_MINUS_SRC_ALPHA;
623 root 1.48 glBegin GL_QUADS;
624 root 1.278 glVertex 0 , 0;
625     glVertex $self->{w}, 0;
626     glVertex $self->{w}, $self->{h};
627     glVertex 0 , $self->{h};
628 root 1.47 glEnd;
629 root 1.50 glDisable GL_BLEND;
630 root 1.47 }
631 root 1.162
632 root 1.259 if ($ENV{CFPLUS_DEBUG} & 1) {
633 root 1.162 glPushMatrix;
634     glColor 1, 1, 0, 1;
635 root 1.278 glTranslate 0.375, 0.375;
636 root 1.162 glBegin GL_LINE_LOOP;
637 root 1.234 glVertex 0 , 0;
638     glVertex $self->{w} - 1, 0;
639     glVertex $self->{w} - 1, $self->{h} - 1;
640     glVertex 0 , $self->{h} - 1;
641 root 1.162 glEnd;
642     glPopMatrix;
643 root 1.340 #CFPlus::UI::Label->new (w => $self->{w}, h => $self->{h}, text => $self, fontsize => 0)->_draw;
644 root 1.162 }
645 root 1.278
646     $self->_draw;
647     glPopMatrix;
648 elmex 1.11 }
649    
650     sub _draw {
651 root 1.38 my ($self) = @_;
652    
653     warn "no draw defined for $self\n";
654 elmex 1.1 }
655 root 1.4
656 root 1.362 my $cntx;#d#
657 root 1.18 sub DESTROY {
658     my ($self) = @_;
659    
660 root 1.340 return if CFPlus::in_destruct;
661 root 1.324
662 root 1.302 eval { $self->destroy };
663     warn "exception during widget destruction: $@" if $@ & $@ != /during global destruction/;
664 root 1.361
665     delete $WIDGET{$self+0};
666 root 1.18 }
667    
668 root 1.39 #############################################################################
669    
670 root 1.340 package CFPlus::UI::DrawBG;
671 root 1.68
672 root 1.340 our @ISA = CFPlus::UI::Base::;
673 root 1.68
674     use strict;
675 root 1.340 use CFPlus::OpenGL;
676 root 1.68
677     sub new {
678     my $class = shift;
679    
680     # range [value, low, high, page]
681    
682     $class->SUPER::new (
683 root 1.209 #bg => [0, 0, 0, 0.2],
684     #active_bg => [1, 1, 1, 0.5],
685 root 1.68 @_
686     )
687     }
688    
689     sub _draw {
690     my ($self) = @_;
691    
692 root 1.209 my $color = $FOCUS == $self && $self->{active_bg}
693     ? $self->{active_bg}
694     : $self->{bg};
695    
696     if ($color && (@$color < 4 || $color->[3])) {
697     my ($w, $h) = @$self{qw(w h)};
698 root 1.68
699 root 1.209 glEnable GL_BLEND;
700 root 1.278 glBlendFunc GL_ONE, GL_ONE_MINUS_SRC_ALPHA;
701     glColor_premultiply @$color;
702 root 1.76
703 root 1.209 glBegin GL_QUADS;
704     glVertex 0 , 0;
705     glVertex 0 , $h;
706     glVertex $w, $h;
707     glVertex $w, 0;
708     glEnd;
709 root 1.76
710 root 1.209 glDisable GL_BLEND;
711     }
712 root 1.68 }
713    
714     #############################################################################
715    
716 root 1.340 package CFPlus::UI::Empty;
717 root 1.66
718 root 1.340 our @ISA = CFPlus::UI::Base::;
719 root 1.66
720 elmex 1.150 sub new {
721     my ($class, %arg) = @_;
722     $class->SUPER::new (can_events => 0, %arg);
723     }
724    
725 root 1.66 sub size_request {
726 root 1.256 my ($self) = @_;
727    
728     ($self->{w} + 0, $self->{h} + 0)
729 root 1.66 }
730    
731 root 1.67 sub draw { }
732 root 1.66
733     #############################################################################
734    
735 root 1.340 package CFPlus::UI::Container;
736 elmex 1.15
737 root 1.340 our @ISA = CFPlus::UI::Base::;
738 elmex 1.15
739 root 1.38 sub new {
740 root 1.64 my ($class, %arg) = @_;
741    
742 root 1.272 my $children = delete $arg{children};
743 root 1.38
744 root 1.166 my $self = $class->SUPER::new (
745     children => [],
746     can_events => 0,
747     %arg,
748     );
749 root 1.272
750     $self->add (@$children)
751     if $children;
752 root 1.38
753     $self
754     }
755    
756 root 1.332 sub realloc {
757     my ($self) = @_;
758    
759     $self->{force_realloc} = 1;
760     $self->{force_size_alloc} = 1;
761     $self->SUPER::realloc;
762     }
763    
764 root 1.38 sub add {
765 root 1.188 my ($self, @widgets) = @_;
766 root 1.113
767 root 1.188 $_->set_parent ($self)
768     for @widgets;
769 root 1.38
770 root 1.113 use sort 'stable';
771 root 1.38
772 root 1.66 $self->{children} = [
773 root 1.38 sort { $a->{z} <=> $b->{z} }
774 root 1.188 @{$self->{children}}, @widgets
775 root 1.66 ];
776 root 1.38
777 root 1.251 $self->realloc;
778 root 1.38 }
779 root 1.35
780 root 1.172 sub children {
781     @{ $_[0]{children} }
782     }
783    
784 elmex 1.32 sub remove {
785 root 1.134 my ($self, $child) = @_;
786    
787     delete $child->{parent};
788 root 1.163 $child->hide;
789 root 1.38
790 root 1.134 $self->{children} = [ grep $_ != $child, @{ $self->{children} } ];
791 root 1.38
792 root 1.251 $self->realloc;
793 root 1.38 }
794    
795 root 1.162 sub clear {
796     my ($self) = @_;
797    
798 root 1.163 my $children = delete $self->{children};
799     $self->{children} = [];
800 root 1.162
801 root 1.163 for (@$children) {
802     delete $_->{parent};
803     $_->hide;
804     }
805 root 1.182
806 root 1.251 $self->realloc;
807 root 1.162 }
808    
809 root 1.38 sub find_widget {
810     my ($self, $x, $y) = @_;
811    
812 root 1.45 $x -= $self->{x};
813     $y -= $self->{y};
814    
815 root 1.38 my $res;
816    
817 root 1.272 for (reverse $self->visible_children) {
818 root 1.45 $res = $_->find_widget ($x, $y)
819 root 1.38 and return $res;
820     }
821    
822 root 1.46 $self->SUPER::find_widget ($x + $self->{x}, $y + $self->{y})
823 elmex 1.32 }
824 elmex 1.15
825 root 1.35 sub _draw {
826     my ($self) = @_;
827    
828 root 1.38 $_->draw for @{$self->{children}};
829 root 1.35 }
830 elmex 1.15
831 root 1.39 #############################################################################
832    
833 root 1.340 package CFPlus::UI::Bin;
834 elmex 1.32
835 root 1.340 our @ISA = CFPlus::UI::Container::;
836 elmex 1.32
837 root 1.66 sub new {
838     my ($class, %arg) = @_;
839    
840 root 1.340 my $child = (delete $arg{child}) || new CFPlus::UI::Empty::;
841 root 1.66
842     $class->SUPER::new (children => [$child], %arg)
843     }
844    
845     sub add {
846 root 1.134 my ($self, $child) = @_;
847 root 1.66
848 root 1.302 $self->SUPER::remove ($_) for @{ $self->{children} };
849 root 1.134 $self->SUPER::add ($child);
850 root 1.66 }
851    
852     sub remove {
853     my ($self, $widget) = @_;
854    
855     $self->SUPER::remove ($widget);
856    
857 root 1.340 $self->{children} = [new CFPlus::UI::Empty]
858 root 1.66 unless @{$self->{children}};
859     }
860    
861 root 1.39 sub child { $_[0]->{children}[0] }
862 elmex 1.32
863 root 1.38 sub size_request {
864 root 1.68 $_[0]{children}[0]->size_request
865 root 1.38 }
866 elmex 1.32
867 root 1.305 sub invoke_size_allocate {
868 root 1.259 my ($self, $w, $h) = @_;
869 root 1.42
870 root 1.128 $self->{children}[0]->configure (0, 0, $w, $h);
871 root 1.305
872     1
873 root 1.38 }
874 elmex 1.32
875 root 1.39 #############################################################################
876    
877 root 1.274 # back-buffered drawing area
878    
879 root 1.340 package CFPlus::UI::Window;
880 elmex 1.20
881 root 1.340 our @ISA = CFPlus::UI::Bin::;
882 elmex 1.20
883 root 1.340 use CFPlus::OpenGL;
884 elmex 1.20
885 root 1.42 sub new {
886 root 1.64 my ($class, %arg) = @_;
887 elmex 1.32
888 root 1.64 my $self = $class->SUPER::new (%arg);
889 elmex 1.32 }
890    
891     sub update {
892     my ($self) = @_;
893 root 1.42
894 root 1.198 $ROOT->on_post_alloc ($self => sub { $self->render_child });
895 root 1.42 $self->SUPER::update;
896 elmex 1.20 }
897    
898 root 1.305 sub invoke_size_allocate {
899 root 1.259 my ($self, $w, $h) = @_;
900 root 1.174
901 root 1.259 $self->update;
902 root 1.305
903     $self->SUPER::invoke_size_allocate ($w, $h)
904 root 1.174 }
905    
906 root 1.182 sub _render {
907 root 1.267 my ($self) = @_;
908    
909     $self->{children}[0]->draw;
910 root 1.182 }
911    
912 root 1.174 sub render_child {
913 elmex 1.20 my ($self) = @_;
914    
915 root 1.340 $self->{texture} = new_from_opengl CFPlus::Texture $self->{w}, $self->{h}, sub {
916 root 1.216 glClearColor 0, 0, 0, 0;
917 root 1.105 glClear GL_COLOR_BUFFER_BIT;
918 root 1.182
919 root 1.267 {
920 root 1.340 package CFPlus::UI::Base;
921 root 1.267
922 root 1.351 local ($draw_x, $draw_y, $draw_w, $draw_h) =
923 root 1.267 (0, 0, $self->{w}, $self->{h});
924 root 1.352
925     $self->_render;
926 root 1.267 }
927 root 1.105 };
928 elmex 1.36 }
929    
930 elmex 1.20 sub _draw {
931     my ($self) = @_;
932    
933     my $tex = $self->{texture}
934     or return;
935    
936     glEnable GL_TEXTURE_2D;
937 root 1.35 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
938 root 1.278 glColor 0, 0, 0, 1;
939 elmex 1.20
940 root 1.344 $tex->draw_quad_alpha_premultiplied (0, 0);
941 elmex 1.20
942     glDisable GL_TEXTURE_2D;
943     }
944    
945 root 1.39 #############################################################################
946    
947 root 1.340 package CFPlus::UI::ViewPort;
948 root 1.125
949 root 1.375 use List::Util qw(min max);
950    
951 root 1.340 our @ISA = CFPlus::UI::Window::;
952 root 1.125
953 root 1.234 sub new {
954     my $class = shift;
955    
956     $class->SUPER::new (
957     scroll_x => 0,
958     scroll_y => 1,
959     @_,
960     )
961     }
962    
963 root 1.125 sub size_request {
964     my ($self) = @_;
965    
966 root 1.259 my ($w, $h) = @{$self->child}{qw(req_w req_h)};
967 root 1.247
968     $w = 10 if $self->{scroll_x};
969     $h = 10 if $self->{scroll_y};
970 root 1.125
971 root 1.247 ($w, $h)
972 root 1.125 }
973    
974 root 1.305 sub invoke_size_allocate {
975 root 1.259 my ($self, $w, $h) = @_;
976    
977     my $child = $self->child;
978 root 1.182
979 root 1.259 $w = $child->{req_w} if $self->{scroll_x} && $child->{req_w};
980     $h = $child->{req_h} if $self->{scroll_y} && $child->{req_h};
981 root 1.234
982     $self->child->configure (0, 0, $w, $h);
983 root 1.182 $self->update;
984 root 1.305
985     1
986 root 1.182 }
987    
988     sub set_offset {
989     my ($self, $x, $y) = @_;
990    
991 root 1.378 my $x = max 0, min $self->child->{w} - $self->{w}, int $x;
992     my $y = max 0, min $self->child->{h} - $self->{h}, int $y;
993 root 1.182
994 root 1.378 if ($x != $self->{view_x} or $y != $self->{view_y}) {
995     $self->{view_x} = $x;
996     $self->{view_y} = $y;
997    
998     $self->emit (changed => $x, $y);
999     $self->update;
1000     }
1001 root 1.182 }
1002    
1003 root 1.191 # hmm, this does not work for topleft of $self... but we should not ask for that
1004     sub coord2local {
1005     my ($self, $x, $y) = @_;
1006    
1007     $self->SUPER::coord2local ($x + $self->{view_x}, $y + $self->{view_y})
1008     }
1009    
1010     sub coord2global {
1011 root 1.184 my ($self, $x, $y) = @_;
1012    
1013 root 1.191 $x = List::Util::min $self->{w}, $x - $self->{view_x};
1014     $y = List::Util::min $self->{h}, $y - $self->{view_y};
1015    
1016     $self->SUPER::coord2global ($x, $y)
1017 root 1.184 }
1018    
1019     sub find_widget {
1020     my ($self, $x, $y) = @_;
1021    
1022     if ( $x >= $self->{x} && $x < $self->{x} + $self->{w}
1023     && $y >= $self->{y} && $y < $self->{y} + $self->{h}
1024     ) {
1025     $self->child->find_widget ($x + $self->{view_x}, $y + $self->{view_y})
1026     } else {
1027 root 1.340 $self->CFPlus::UI::Base::find_widget ($x, $y)
1028 root 1.184 }
1029     }
1030    
1031 root 1.182 sub _render {
1032 root 1.125 my ($self) = @_;
1033    
1034 root 1.340 local $CFPlus::UI::Base::draw_x = $CFPlus::UI::Base::draw_x - $self->{view_x};
1035     local $CFPlus::UI::Base::draw_y = $CFPlus::UI::Base::draw_y - $self->{view_y};
1036 root 1.267
1037 root 1.340 CFPlus::OpenGL::glTranslate -$self->{view_x}, -$self->{view_y};
1038 root 1.182
1039     $self->SUPER::_render;
1040     }
1041    
1042     #############################################################################
1043    
1044 root 1.340 package CFPlus::UI::ScrolledWindow;
1045 root 1.182
1046 root 1.369 our @ISA = CFPlus::UI::Table::;
1047 root 1.182
1048     sub new {
1049 root 1.273 my ($class, %arg) = @_;
1050    
1051     my $child = delete $arg{child};
1052 root 1.182
1053     my $self;
1054    
1055 root 1.369 my $hslider = new CFPlus::UI::Slider
1056     vertical => 0,
1057     range => [0, 0, 1, 0.01], # HACK fix
1058     on_changed => sub {
1059     $self->{hpos} = $_[1];
1060     $self->{vp}->set_offset ($self->{hpos}, $self->{vpos});
1061     },
1062     ;
1063    
1064     my $vslider = new CFPlus::UI::Slider
1065 root 1.243 vertical => 1,
1066     range => [0, 0, 1, 0.01], # HACK fix
1067     on_changed => sub {
1068 root 1.369 $self->{vpos} = $_[1];
1069     $self->{vp}->set_offset ($self->{hpos}, $self->{vpos});
1070 root 1.182 },
1071     ;
1072    
1073     $self = $class->SUPER::new (
1074 root 1.372 scroll_x => 0,
1075     scroll_y => 1,
1076 root 1.330 can_events => 1,
1077 root 1.369 hslider => $hslider,
1078     vslider => $vslider,
1079     col_expand => [1, 0],
1080     row_expand => [1, 0],
1081 root 1.273 %arg,
1082 root 1.182 );
1083    
1084 root 1.372 $self->{vp} = new CFPlus::UI::ViewPort
1085 root 1.376 expand => 1,
1086     scroll_x => $self->{scroll_x},
1087     scroll_y => $self->{scroll_y},
1088     on_changed => sub {
1089     my ($vp, $x, $y) = @_;
1090 root 1.377
1091 root 1.376 $vp->{parent}{hslider}->set_value ($x);
1092     $vp->{parent}{vslider}->set_value ($y);
1093    
1094     0
1095     },
1096 root 1.372 ;
1097    
1098 root 1.383 $self->SUPER::add_at (0, 0, $self->{vp});
1099 root 1.369
1100 root 1.273 $self->add ($child) if $child;
1101 root 1.182
1102     $self
1103 root 1.125 }
1104    
1105 root 1.314 #TODO# update range on size_allocate depending on child
1106    
1107 root 1.273 sub add {
1108     my ($self, $widget) = @_;
1109    
1110     $self->{vp}->add ($self->{child} = $widget);
1111     }
1112    
1113 root 1.314 sub update_slider {
1114     my ($self) = @_;
1115    
1116 root 1.371 my $child = ($self->{vp} or return)->child;
1117    
1118     my ($w1, $w2) = ($child->{w}, $self->{vp}{w});
1119 root 1.369 $self->{hslider}->set_range ([$self->{hslider}{range}[0], 0, $w1, $w2, 1]);
1120 root 1.374
1121     my $visible = $w1 > $w2;
1122     if ($visible != $self->{hslider}{visible}) {
1123 root 1.383 $visible ? $self->SUPER::add_at (0, 1, $self->{hslider})
1124 root 1.374 : $self->{hslider}->hide;
1125     }
1126 root 1.369
1127 root 1.371 my ($h1, $h2) = ($child->{h}, $self->{vp}{h});
1128 root 1.369 $self->{vslider}->set_range ([$self->{vslider}{range}[0], 0, $h1, $h2, 1]);
1129 root 1.374
1130     my $visible = $h1 > $h2;
1131     if ($visible != $self->{vslider}{visible}) {
1132 root 1.383 $visible ? $self->SUPER::add_at (1, 0, $self->{vslider})
1133 root 1.384 #!/opt/bin/perl
1134    
1135     my $startup_done = sub { };
1136     our $PANGO = "1.5.0";
1137    
1138     # do splash-screen thingy on win32
1139     BEGIN {
1140     if (%PAR::LibCache && $^O eq "MSWin32") {
1141     while (my ($filename, $zip) = each %PAR::LibCache) {
1142     $zip->extractMember ("SPLASH.bmp", "$ENV{PAR_TEMP}/SPLASH.bmp");
1143     }
1144    
1145     require Win32::GUI::SplashScreen;
1146    
1147     Win32::GUI::SplashScreen::Show (
1148     -file => "$ENV{PAR_TEMP}/SPLASH.bmp",
1149     );
1150    
1151     $startup_done = sub {
1152     Win32::GUI::SplashScreen::Done (1);
1153     };
1154     }
1155     }
1156    
1157     use strict;
1158     use utf8;
1159    
1160     use Carp 'verbose';
1161    
1162     # do things only needed for single-binary version (par)
1163     BEGIN {
1164     if (%PAR::LibCache) {
1165     @INC = grep ref, @INC; # weed out all paths except pars loader refs
1166    
1167     my $tmp = $ENV{PAR_TEMP};
1168    
1169     while (my ($filename, $zip) = each %PAR::LibCache) {
1170     for ($zip->memberNames) {
1171     next unless /^root\/(.*)/;
1172     $zip->extractMember ($_, "$tmp/$1")
1173     unless -e "$tmp/$1";
1174     }
1175     }
1176    
1177     if ($^O eq "MSWin32") {
1178     # relocatable
1179     } else {
1180     # unix, need to patch pango rc file
1181     open my $fh, "<:perlio", "$tmp/usr/lib/pango/$PANGO/module-files.d/libpango1.0-0.modules"
1182     or die "$tmp/usr/lib/$PANGO/module-files.d/libpango1.0-0.modules: $!";
1183     local $/;
1184     my $rc = <$fh>;
1185     $rc =~ s/^\//$tmp\//gm; # replace abs paths by relative ones
1186    
1187     mkdir "$tmp/pango-modules";
1188     open my $fh, ">:perlio", "$tmp/pango-modules/pango.modules"
1189     or die "$tmp/pango-modules/pango.modules: $!";
1190     print $fh $rc;
1191    
1192     $ENV{PANGO_RC_FILE} = "$tmp/pango.rc";
1193     open my $fh, ">:perlio", $ENV{PANGO_RC_FILE}
1194     or die "$ENV{PANGO_RC_FILE}: $!";
1195     print $fh "[Pango]\nModuleFiles = $tmp/pango-modules\n";
1196     }
1197    
1198     unshift @INC, $tmp;
1199     }
1200     }
1201    
1202     # need to do it again because that pile of garbage called PAR nukes it before main
1203     unshift @INC, $ENV{PAR_TEMP}
1204     if %PAR::LibCache;
1205    
1206     use Time::HiRes 'time';
1207     use Event;
1208    
1209     use Crossfire;
1210     use Crossfire::Protocol::Constants;
1211    
1212     use Compress::LZF;
1213    
1214     use CFPlus;
1215     use CFPlus::OpenGL ();
1216     use CFPlus::Protocol;
1217     use CFPlus::DB;
1218     use CFPlus::UI;
1219     use CFPlus::UI::Inventory;
1220     use CFPlus::UI::SpellList;
1221     use CFPlus::Pod;
1222     use CFPlus::MapWidget;
1223     use CFPlus::Macro;
1224    
1225     $SIG{QUIT} = sub { Carp::cluck "QUIT" };
1226     $SIG{PIPE} = 'IGNORE';
1227    
1228     $Event::Eval = 1;
1229     $Event::DIED = sub {
1230     CFPlus::fatal Carp::longmess $_[1]
1231     };
1232    
1233     my $MAX_FPS = 60;
1234     my $MIN_FPS = 5; # unused as of yet
1235    
1236     our $META_SERVER = "http://metaserver.schmorp.de/current.json";
1237    
1238     our $LAST_REFRESH;
1239     our $NOW;
1240    
1241     our $CFG;
1242     our $CONN;
1243     our $PROFILE; # current profile
1244     our $FAST; # fast, low-quality mode, possibly useful for software-rendering
1245    
1246     our $WANT_REFRESH;
1247     our $CAN_REFRESH;
1248    
1249     our @SDL_MODES;
1250     our $WIDTH;
1251     our $HEIGHT;
1252     our $FULLSCREEN;
1253     our $FONTSIZE;
1254    
1255     our $FONT_PROP;
1256     our $FONT_FIXED;
1257    
1258     our $MAP;
1259     our $MAPMAP;
1260     our $MAPWIDGET;
1261     our $BUTTONBAR;
1262     our $LOGVIEW;
1263     our $CONSOLE;
1264     our $METASERVER;
1265     our $LOGIN_BUTTON;
1266     our $QUIT_DIALOG;
1267     our $HOST_ENTRY;
1268     our $FULLSCREEN_ENABLE;
1269     our $PICKUP_ENABLE;
1270     our $SERVER_INFO;
1271    
1272     our $SETUP_DIALOG;
1273     our $SETUP_NOTEBOOK;
1274     our $SETUP_SERVER;
1275     our $SETUP_KEYBOARD;
1276    
1277     our $PL_NOTEBOOK;
1278     our $PL_WINDOW;
1279    
1280     our $INVENTORY_PAGE;
1281     our $STATS_PAGE;
1282     our $SKILL_PAGE;
1283     our $SPELL_PAGE;
1284     our $SPELL_LIST;
1285    
1286     our $HELP_WINDOW;
1287     our $MESSAGE_WINDOW;
1288     our $FLOORBOX;
1289     our $GAUGES;
1290     our $STATWIDS;
1291    
1292     our $SDL_ACTIVE;
1293     our %SDL_CB;
1294    
1295     our $SDL_MIXER;
1296     our $MUSIC_DEFAULT = "in_a_heartbeat.ogg";
1297     our @MUSIC_WANT;
1298     our $MUSIC_START;
1299     our $MUSIC_PLAYING;
1300     our $MUSIC_PLAYER;
1301     our $MUSIC_RESUME = 30; # resume music when players less than these many seconds before
1302     our @SOUNDS; # event => file mapping
1303     our %AUDIO_CHUNKS; # audio files
1304    
1305     our $ALT_ENTER_MESSAGE;
1306     our $STATUSBOX;
1307     our $DEBUG_STATUS;
1308    
1309     our $INV;
1310     our $INVR;
1311     our $INV_RIGHT_HB;
1312    
1313     our $PICKUP_CFG;
1314    
1315     our $IN_BUILD_MODE;
1316     our $BUILD_BUTTON;
1317    
1318     sub status {
1319     $STATUSBOX->add (CFPlus::asxml $_[0], pri => -10, group => "status", timeout => 10, fg => [1, 1, 0, 1]);
1320     }
1321    
1322     sub debug {
1323     $DEBUG_STATUS->set_text ($_[0]);
1324     }
1325    
1326     sub message {
1327     my ($para) = @_;
1328    
1329     my $time = sprintf "%02d:%02d:%02d", (localtime time)[2,1,0];
1330    
1331     $para->{markup} = "<span foreground='#ffffff'>$time</span> $para->{markup}";
1332    
1333     $LOGVIEW->add_paragraph ($para);
1334     $LOGVIEW->scroll_to_bottom;
1335     }
1336    
1337     sub destroy_query_dialog {
1338     (delete $_[0]{query_dialog})->destroy
1339     if $_[0]{query_dialog};
1340     }
1341    
1342     # FIXME: a very ugly hack to wait for stat update look below! #d#
1343     our $QUERY_TIMER; #d#
1344    
1345     # server query dialog
1346     sub server_query {
1347     my ($conn, $flags, $prompt) = @_;
1348    
1349     # FIXME: a very ugly hack to wait for stat update #d#
1350     if ($prompt =~ /roll new stats/ and not $conn->{stat_change_with}) {
1351     unless ($QUERY_TIMER) {
1352     $QUERY_TIMER =
1353     Event->timer (
1354     after => 1,
1355     cb => sub {
1356     server_query ($conn, $flags, $prompt, 1);
1357     $QUERY_TIMER = undef
1358     }
1359     );
1360     return;
1361     }
1362     }
1363    
1364     $conn->{query_dialog} = my $dialog = new CFPlus::UI::Toplevel
1365     x => "center",
1366     y => "center",
1367     title => "Server Query",
1368     child => my $vbox = new CFPlus::UI::VBox,
1369     ;
1370    
1371     my @dialog = my $label = new CFPlus::UI::Label
1372     max_w => $::WIDTH * 0.8,
1373     ellipsise => 0,
1374     text => $prompt;
1375    
1376     if ($flags & CS_QUERY_YESNO) {
1377     push @dialog, my $hbox = new CFPlus::UI::HBox;
1378    
1379     $hbox->add (new CFPlus::UI::Button
1380     text => "No",
1381     on_activate => sub {
1382     $conn->send ("reply n");
1383     $dialog->destroy;
1384     0
1385     }
1386     );
1387     $hbox->add (new CFPlus::UI::Button
1388     text => "Yes",
1389     on_activate => sub {
1390     $conn->send ("reply y");
1391     destroy_query_dialog $conn;
1392     0
1393     },
1394     );
1395    
1396     $dialog->grab_focus;
1397    
1398     } elsif ($flags & CS_QUERY_SINGLECHAR) {
1399     if ($prompt =~ /Now choose a character|Press any key for the next race/i) {
1400     $dialog->{tooltip} = "#charcreation_focus";
1401    
1402     unshift @dialog, new CFPlus::UI::Label
1403     max_w => $::WIDTH * 0.8,
1404     ellipsise => 0,
1405     markup => "\nOr use your keyboard and the text entry below:\n";
1406    
1407     unshift @dialog, my $table = new CFPlus::UI::Table;
1408    
1409     $table->add_at (0, 0, new CFPlus::UI::Button
1410     text => "Next Race",
1411     on_activate => sub {
1412     $conn->send ("reply n");
1413     destroy_query_dialog $conn;
1414     0
1415     },
1416     );
1417     $table->add_at (2, 0, new CFPlus::UI::Button
1418     text => "Accept",
1419     on_activate => sub {
1420     $conn->send ("reply d");
1421     destroy_query_dialog $conn;
1422     0
1423     },
1424     );
1425    
1426     if ($conn->{chargen_race_description}) {
1427     unshift @dialog, new CFPlus::UI::Label
1428     max_w => $::WIDTH * 0.8,
1429     ellipsise => 0,
1430     markup => "<span foreground='#ccccff'>$conn->{chargen_race_description}</span>",
1431     ;
1432     }
1433    
1434     unshift @dialog, new CFPlus::UI::Face
1435     face => $conn->{player}{face},
1436     bg => [.2, .2, .2, 1],
1437     min_w => 64,
1438     min_h => 64,
1439     ;
1440    
1441     if ($conn->{chargen_race_title}) {
1442     unshift @dialog, new CFPlus::UI::Label
1443     allign => 1,
1444     ellipsise => 0,
1445     markup => "<span foreground='#ccccff' size='large'>Race: $conn->{chargen_race_title}</span>",
1446     ;
1447     }
1448    
1449     unshift @dialog, new CFPlus::UI::Label
1450     max_w => $::WIDTH * 0.4,
1451     ellipsise => 0,
1452     markup => (CFPlus::Pod::section_label ui => "chargen_race"),
1453     ;
1454    
1455     } elsif ($prompt =~ /roll new stats/) {
1456     if (my $stat = delete $conn->{stat_change_with}) {
1457     $conn->send ("reply $stat");
1458     destroy_query_dialog $conn;
1459     return;
1460     }
1461    
1462     unshift @dialog, new CFPlus::UI::Label
1463     max_w => $::WIDTH * 0.4,
1464     ellipsise => 0,
1465     markup => "\nOr use your keyboard and the text entry below:\n";
1466    
1467     unshift @dialog, my $table = new CFPlus::UI::Table;
1468    
1469     # left: re-roll
1470     $table->add_at (0, 0, new CFPlus::UI::Button
1471     text => "Roll Again",
1472     on_activate => sub {
1473     $conn->send ("reply y");
1474     destroy_query_dialog $conn;
1475     0
1476     },
1477     );
1478    
1479     # center: swap stats
1480     my ($sw1, $sw2) = map +(new CFPlus::UI::Selector
1481     expand => 1,
1482     value => $_,
1483     options => [
1484     [1 => "Str", "Strength ($conn->{stat}{+CS_STAT_STR})"],
1485     [2 => "Dex", "Dexterity ($conn->{stat}{+CS_STAT_DEX})"],
1486     [3 => "Con", "Constitution ($conn->{stat}{+CS_STAT_CON})"],
1487     [4 => "Int", "Intelligence ($conn->{stat}{+CS_STAT_INT})"],
1488     [5 => "Wis", "Wisdom ($conn->{stat}{+CS_STAT_WIS})"],
1489     [6 => "Pow", "Power ($conn->{stat}{+CS_STAT_POW})"],
1490     [7 => "Cha", "Charisma ($conn->{stat}{+CS_STAT_CHA})"],
1491     ],
1492     ), 1 .. 2;
1493    
1494     $table->add_at (2, 0, new CFPlus::UI::Button
1495     text => "Swap Stats",
1496     on_activate => sub {
1497     $conn->{stat_change_with} = $sw2->{value};
1498     $conn->send ("reply $sw1->{value}");
1499     destroy_query_dialog $conn;
1500     0
1501     },
1502     );
1503     $table->add_at (2, 1, new CFPlus::UI::HBox children => [$sw1, $sw2]);
1504    
1505     # right: accept
1506     $table->add_at (4, 0, new CFPlus::UI::Button
1507     text => "Accept",
1508     on_activate => sub {
1509     $conn->send ("reply n");
1510     $STATS_PAGE->hide;
1511     destroy_query_dialog $conn;
1512     0
1513     },
1514     );
1515    
1516     unshift @dialog, my $hbox = new CFPlus::UI::HBox;
1517     for (
1518     [Str => CS_STAT_STR],
1519     [Dex => CS_STAT_DEX],
1520     [Con => CS_STAT_CON],
1521     [Int => CS_STAT_INT],
1522     [Wis => CS_STAT_WIS],
1523     [Pow => CS_STAT_POW],
1524     [Cha => CS_STAT_CHA],
1525     ) {
1526     my ($name, $id) = @$_;
1527     $hbox->add (new CFPlus::UI::Label
1528     markup => "$conn->{stat}{$id} <span foreground='yellow'>$name</span>",
1529     align => 0,
1530     expand => 1,
1531     can_events => 1,
1532     can_hover => 1,
1533     tooltip => "#stat_$name",
1534     );
1535     }
1536    
1537     unshift @dialog, new CFPlus::UI::Label
1538     max_w => $::WIDTH * 0.4,
1539     ellipsise => 0,
1540     markup => (CFPlus::Pod::section_label ui => "chargen_stats"),
1541     ;
1542     }
1543    
1544     push @dialog, my $entry = new CFPlus::UI::Entry
1545     on_changed => sub {
1546     $conn->send ("reply $_[1]");
1547     destroy_query_dialog $conn;
1548     0
1549     },
1550     ;
1551    
1552     $entry->grab_focus;
1553    
1554     } else {
1555     $dialog->{tooltip} = "Enter the reply and press return (click on the entry to make sure it has keyboard focus)";
1556    
1557     push @dialog, my $entry = new CFPlus::UI::Entry
1558     $flags & CS_QUERY_HIDEINPUT ? (hidden => "*") : (),
1559     on_activate => sub {
1560     $conn->send ("reply $_[1]");
1561     destroy_query_dialog $conn;
1562     0
1563     },
1564     ;
1565    
1566     $entry->grab_focus;
1567     }
1568    
1569     $vbox->add (@dialog);
1570     $dialog->show;
1571     }
1572    
1573     sub start_game {
1574     status "logging in...";
1575    
1576     $LOGIN_BUTTON->set_text ("Logout");
1577     $SETUP_DIALOG->hide;
1578    
1579     my $mapsize = List::Util::min 32, List::Util::max 11, int $WIDTH * $CFG->{mapsize} * 0.01 / 32;
1580    
1581     my ($host, $port) = split /:/, $PROFILE->{host};
1582    
1583     $MAP = new CFPlus::Map;
1584    
1585     $CONN = eval {
1586     new CFPlus::Protocol
1587     host => $host,
1588     port => $port || 13327,
1589     user => $PROFILE->{user},
1590     pass => $PROFILE->{password},
1591     mapw => $mapsize,
1592     maph => $mapsize,
1593    
1594     client => "cfplus $CFPlus::VERSION $] $^O",
1595    
1596     map_widget => $MAPWIDGET,
1597     logview => $LOGVIEW,
1598     statusbox => $STATUSBOX,
1599     map => $MAP,
1600     mapmap => $MAPMAP,
1601     query => \&server_query,
1602    
1603     setup_req => {
1604     smoothing => $CFG->{map_smoothing}*1,
1605     },
1606    
1607     sound_play => sub {
1608     my ($x, $y, $soundnum, $type) = @_;
1609    
1610     $SDL_MIXER
1611     or return;
1612    
1613     my $chunk = $AUDIO_CHUNKS{$SOUNDS[$soundnum]}
1614     or return;
1615    
1616     $chunk->play;
1617     },
1618     };
1619    
1620     if ($CONN) {
1621     CFPlus::lowdelay fileno $CONN->{fh};
1622    
1623     status "login successful";
1624     } else {
1625     status "unable to connect";
1626     stop_game();
1627     }
1628     }
1629    
1630     sub stop_game {
1631     $LOGIN_BUTTON->set_text ("Login");
1632     $SETUP_NOTEBOOK->set_current_page ($SETUP_SERVER);
1633     $SETUP_DIALOG->show;
1634     $PL_WINDOW->hide;
1635     $SPELL_LIST->clear_spells;
1636     $CFPlus::UI::ROOT->emit (stop_game => ! ! $CONN);
1637    
1638     &audio_music_set ([]);
1639    
1640     return unless $CONN;
1641    
1642     status "connection closed";
1643    
1644     destroy_query_dialog $CONN;
1645     $CONN->destroy;
1646     $CONN = 0; # false, does not autovivify
1647    
1648     undef $MAP;
1649     }
1650    
1651     sub graphics_setup {
1652     my $vbox = new CFPlus::UI::VBox;
1653    
1654     $vbox->add (my $table = new CFPlus::UI::Table expand => 1, col_expand => [0, 1]);
1655    
1656     my $row = 0;
1657    
1658     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "OpenGL Info");
1659     $table->add_at (1, $row++, new CFPlus::UI::Label valign => 0, fontsize => 0.8, text => CFPlus::OpenGL::gl_vendor . ", " . CFPlus::OpenGL::gl_version,
1660     can_events => 1,
1661     tooltip => "<tt><span size='8192'>" . (CFPlus::OpenGL::gl_extensions) . "</span></tt>");
1662    
1663     my $vidmode_tooltip =
1664     "<b>Video Mode.</b> The video mode to use for fullscreen (and the window size for windowed operation). "
1665     . "The format is <i>width</i> x <i>height</i> \@ <i>depth-per-channel</i> + <i>alpha-channel</i>.";
1666    
1667     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Video Mode");
1668     $table->add_at (1, $row++, my $hbox = new CFPlus::UI::HBox);
1669    
1670     $hbox->add (my $mode_slider = new CFPlus::UI::Slider
1671     force_w => $WIDTH * 0.1, expand => 1, range => [$CFG->{sdl_mode}, 0, $#SDL_MODES, 0, 1],
1672     tooltip => $vidmode_tooltip);
1673     $hbox->add (my $mode_label = new CFPlus::UI::Label
1674     align => 0, valign => 0, height => 0.8, template => "9999x9999@9+9",
1675     can_events => 1, tooltip => $vidmode_tooltip);
1676    
1677     $mode_slider->connect (changed => sub {
1678     my ($self, $value) = @_;
1679    
1680     $CFG->{sdl_mode} = $self->{range}[0] = $value = int $value;
1681     $mode_label->set_text (sprintf '%dx%d@%d+%d', @{$SDL_MODES[$value]});
1682     });
1683     $mode_slider->emit (changed => $mode_slider->{range}[0]);
1684    
1685     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Fullscreen");
1686     $table->add_at (1, $row++, $FULLSCREEN_ENABLE = new CFPlus::UI::CheckBox
1687     state => $CFG->{fullscreen},
1688     tooltip => "Bring the client into fullscreen mode.",
1689     on_changed => sub { my ($self, $value) = @_; $CFG->{fullscreen} = $value; 0 }
1690     );
1691    
1692     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Fast & Ugly");
1693     $table->add_at (1, $row++, new CFPlus::UI::CheckBox
1694     state => $CFG->{fast},
1695     tooltip => "Lower the visual quality considerably to speed up rendering.",
1696     on_changed => sub { my ($self, $value) = @_; $CFG->{fast} = $value; 0 }
1697     );
1698    
1699     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "GUI Fontsize");
1700     $table->add_at (1, $row++, new CFPlus::UI::Slider
1701     range => [$CFG->{gui_fontsize}, 0.5, 2, 0, 0.1],
1702     tooltip => "The base font size used by most GUI elements that do not have their own setting.",
1703     on_changed => sub { $CFG->{gui_fontsize} = $_[1]; 0 },
1704     );
1705    
1706     $table->add_at (1, $row++, new CFPlus::UI::Button
1707     expand => 1, align => 0, text => "Apply",
1708     tooltip => "Apply the video settings above.",
1709     on_activate => sub {
1710     video_shutdown ();
1711     video_init ();
1712     0
1713     }
1714     );
1715    
1716     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Map Scale");
1717     $table->add_at (1, $row++, new CFPlus::UI::Slider
1718     range => [(log $CFG->{map_scale}) / (log 2), -3, 1, 0, 1],
1719     tooltip => "Enlarge or shrink the displayed map. Changes are instant.",
1720     on_changed => sub { my ($self, $value) = @_; $CFG->{map_scale} = 2 ** $value; 0 }
1721     );
1722    
1723     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Map Smoothing");
1724     $table->add_at (1, $row++, new CFPlus::UI::CheckBox
1725     state => $CFG->{map_smoothing},
1726     tooltip => "<b>Map Smoothing</b> tries to make tile borders less square. "
1727     . "This increases load on the graphics subsystem and works only with 2.x servers. "
1728     . "Changes take effect at next connection only.",
1729     on_changed => sub { my ($self, $value) = @_; $CFG->{map_smoothing} = $value; 0 }
1730     );
1731    
1732     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Fog of War");
1733     $table->add_at (1, $row++, new CFPlus::UI::CheckBox
1734     state => $CFG->{fow_enable},
1735     tooltip => "<b>Fog-of-War</b> marks areas that cannot be seen by the player. Changes are instant.",
1736     on_changed => sub { my ($self, $value) = @_; $CFG->{fow_enable} = $value; 0 }
1737     );
1738    
1739     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "FoW Intensity");
1740     $table->add_at (1, $row++, new CFPlus::UI::Slider
1741     range => [$CFG->{fow_intensity}, 0, 1, 0, 1 / 256],
1742     tooltip => "<b>Fog of War Lightness.</b> The higher the intensity, the lighter the Fog-of-War color. Changes are instant.",
1743     on_changed => sub { my ($self, $value) = @_; $CFG->{fow_intensity} = $value; 0 }
1744     );
1745    
1746     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Message Fontsize");
1747     $table->add_at (1, $row++, new CFPlus::UI::Slider
1748     range => [$CFG->{log_fontsize}, 0.5, 2, 0, 0.1],
1749     tooltip => "The font size used by the <b>message/server log</b> window only. Changes are instant.",
1750     on_changed => sub { $LOGVIEW->set_fontsize ($CFG->{log_fontsize} = $_[1]); 0 },
1751     );
1752    
1753     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Gauge fontsize");
1754     $table->add_at (1, $row++, new CFPlus::UI::Slider
1755     range => [$CFG->{gauge_fontsize}, 0.5, 2, 0, 0.1],
1756     tooltip => "Adjusts the fontsize of the gauges at the bottom right. Changes are instant.",
1757     on_changed => sub {
1758     $CFG->{gauge_fontsize} = $_[1];
1759     &set_gauge_window_fontsize;
1760     0
1761     }
1762     );
1763    
1764     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Gauge size");
1765     $table->add_at (1, $row++, new CFPlus::UI::Slider
1766     range => [$CFG->{gauge_size}, 0.2, 0.8],
1767     tooltip => "Adjust the size of the stats gauges at the bottom right. Changes are instant.",
1768     on_changed => sub {
1769     $CFG->{gauge_size} = $_[1];
1770     $GAUGES->{win}->set_size ($WIDTH, int $HEIGHT * $CFG->{gauge_size});
1771     0
1772     }
1773     );
1774    
1775     $vbox
1776     }
1777    
1778     sub audio_setup {
1779     my $vbox = new CFPlus::UI::VBox;
1780    
1781     $vbox->add (my $table = new CFPlus::UI::Table expand => 1, col_expand => [0, 1]);
1782    
1783     my $row = 0;
1784    
1785     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Audio Enable");
1786     $table->add_at (1, $row++, new CFPlus::UI::CheckBox
1787     state => $CFG->{audio_enable},
1788     tooltip => "<b>Master Audio Enable.</b> If enabled, sound effects and music will be played. If disabled, no audio will be used and the soundcard will not be opened.",
1789     on_changed => sub { $CFG->{audio_enable} = $_[1]; 0 }
1790     );
1791     # $table->add_at (0, 9, new CFPlus::UI::Label valign => 0, align => 1, text => "Effects Volume");
1792     # $table->add_at (1, 8, new CFPlus::UI::Slider range => [$CFG->{effects_volume}, 0, 128, 1], on_changed => sub {
1793     # $CFG->{effects_volume} = $_[1];
1794     # });
1795     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Background Music");
1796     $table->add_at (1, $row++, my $hbox = new CFPlus::UI::HBox);
1797     $hbox->add (new CFPlus::UI::CheckBox
1798     expand => 1, state => $CFG->{bgm_enable},
1799     tooltip => "If enabled, playing of background music is enabled. If disabled, no background music will be played.",
1800     on_changed => sub { $CFG->{bgm_enable} = $_[1]; 0 }
1801     );
1802     $hbox->add (new CFPlus::UI::Slider
1803     expand => 1, range => [$CFG->{bgm_volume}, 0, 1, 0, 1/128],
1804     tooltip => "The volume of the background music. Changes are instant.",
1805     on_changed => sub { $CFG->{bgm_volume} = $_[1]; CFPlus::MixMusic::volume $_[1] * 128; 0 }
1806     );
1807    
1808     $table->add_at (1, $row++, new CFPlus::UI::Button
1809     expand => 1, align => 0, text => "Apply",
1810     tooltip => "Apply the audio settings",
1811     on_activate => sub {
1812     audio_shutdown ();
1813     audio_init ();
1814     0
1815     }
1816     );
1817    
1818     $vbox
1819     }
1820    
1821     sub set_gauge_window_fontsize {
1822     for (map { $GAUGES->{$_} } grep { $_ ne 'win' } keys %{$GAUGES}) {
1823     $_->set_fontsize ($::CFG->{gauge_fontsize});
1824     }
1825     }
1826    
1827     sub make_gauge_window {
1828     my $gh = int $HEIGHT * $CFG->{gauge_size};
1829    
1830     my $win = new CFPlus::UI::Frame (
1831     force_x => 0,
1832     force_y => "max",
1833     force_w => $WIDTH,
1834     force_h => $gh,
1835     );
1836    
1837     $win->add (my $hbox = new CFPlus::UI::HBox
1838     children => [
1839     (new CFPlus::UI::HBox expand => 1),
1840     (new CFPlus::UI::VBox children => [
1841     (new CFPlus::UI::Empty expand => 1),
1842     (new CFPlus::UI::Frame bg => [0, 0, 0, 0.4], child => ($FLOORBOX = new CFPlus::UI::Table)),
1843     ]),
1844     (my $vbox = new CFPlus::UI::VBox),
1845     ],
1846     );
1847    
1848     $vbox->add (new CFPlus::UI::HBox
1849     expand => 1,
1850     children => [
1851     (new CFPlus::UI::Empty expand => 1),
1852     (my $hb = new CFPlus::UI::HBox),
1853     ],
1854     );
1855    
1856     $hb->add (my $hg = new CFPlus::UI::Gauge type => 'hp', tooltip => "#stat_health");
1857     $hb->add (my $mg = new CFPlus::UI::Gauge type => 'mana', tooltip => "#stat_mana");
1858     $hb->add (my $gg = new CFPlus::UI::Gauge type => 'grace', tooltip => "#stat_grace");
1859     $hb->add (my $fg = new CFPlus::UI::Gauge type => 'food', tooltip => "#stat_food");
1860    
1861     $vbox->add (my $exp = new CFPlus::UI::Label valign => 0, align => 1, can_hover => 1, can_events => 1, tooltip => "#stat_exp");
1862     $vbox->add (my $rng = new CFPlus::UI::Label valign => 0, align => 1, can_hover => 1, can_events => 1, tooltip => "#stat_ranged");
1863    
1864     $GAUGES = {
1865     exp => $exp, win => $win, range => $rng,
1866     food => $fg, mana => $mg, hp => $hg, grace => $gg
1867     };
1868    
1869     &set_gauge_window_fontsize;
1870    
1871     $win
1872     }
1873    
1874     sub debug_setup {
1875     my $table = new CFPlus::UI::Table;
1876    
1877     $table->add_at (0, 0, new CFPlus::UI::Label text => "Widget Borders");
1878     $table->add_at (1, 0, new CFPlus::UI::CheckBox on_changed => sub { $ENV{CFPLUS_DEBUG} ^= 1; 0 });
1879     $table->add_at (0, 1, new CFPlus::UI::Label text => "Tooltip Widget Info");
1880     $table->add_at (1, 1, new CFPlus::UI::CheckBox on_changed => sub { $ENV{CFPLUS_DEBUG} ^= 2; 0 });
1881     $table->add_at (0, 2, new CFPlus::UI::Label text => "Show FPS");
1882     $table->add_at (1, 2, new CFPlus::UI::CheckBox on_changed => sub { $ENV{CFPLUS_DEBUG} ^= 4; 0 });
1883     $table->add_at (0, 3, new CFPlus::UI::Label text => "Suppress Tooltips");
1884     $table->add_at (1, 3, new CFPlus::UI::CheckBox on_changed => sub { $ENV{CFPLUS_DEBUG} ^= 8; 0 });
1885     $table->add_at (0, 4, new CFPlus::UI::Button text => "die on click(tm)", on_activate => sub { &CFPlus::debug() } );
1886    
1887     $table->add_at (0, 5, new CFPlus::UI::TextEdit text => "line1\0152\0153");#d#
1888    
1889     $table
1890     }
1891    
1892     sub stats_window {
1893     my $r = new CFPlus::UI::ScrolledWindow (
1894     expand => 1,
1895     scroll_y => 1
1896     );
1897     $r->add (my $vb = new CFPlus::UI::VBox);
1898    
1899     $vb->add (new CFPlus::UI::FancyFrame
1900     label => "Player",
1901     child => (my $pi = new CFPlus::UI::VBox),
1902     );
1903    
1904     $pi->add ($STATWIDS->{title} = new CFPlus::UI::Label valign => 0, align => -1, text => "Title:", expand => 1,
1905     can_hover => 1, can_events => 1,
1906     tooltip => "Your name and title. You can change your title by using the <b>title</b> command, if supported by the server.");
1907     $pi->add ($STATWIDS->{map} = new CFPlus::UI::Label valign => 0, align => -1, text => "Map:", expand => 1,
1908     can_hover => 1, can_events => 1,
1909     tooltip => "The map you are currently on (if supported by the server).");
1910    
1911     $pi->add (my $hb0 = new CFPlus::UI::HBox);
1912     $hb0->add ($STATWIDS->{weight} = new CFPlus::UI::Label valign => 0, align => -1, text => "Weight:", expand => 1,
1913     can_hover => 1, can_events => 1,
1914     tooltip => "The weight of the player including all inventory items.");
1915     $hb0->add ($STATWIDS->{m_weight} = new CFPlus::UI::Label valign => 0, align => -1, text => "Max weight:", expand => 1,
1916     can_hover => 1, can_events => 1,
1917     tooltip => "The weight limit: you cannot carry more than this.");
1918    
1919     $vb->add (new CFPlus::UI::FancyFrame
1920     label => "Primary/Secondary Statistics",
1921     child => (my $hb = new CFPlus::UI::HBox expand => 1),
1922     );
1923     $hb->add (my $tbl = new CFPlus::UI::Table expand => 1);
1924    
1925     my $color2 = [1, 1, 0];
1926    
1927     for (
1928     [0, 0, st_str => "Str", 30],
1929     [0, 1, st_dex => "Dex", 30],
1930     [0, 2, st_con => "Con", 30],
1931     [0, 3, st_int => "Int", 30],
1932     [0, 4, st_wis => "Wis", 30],
1933     [0, 5, st_pow => "Pow", 30],
1934     [0, 6, st_cha => "Cha", 30],
1935    
1936     [2, 0, st_wc => "Wc", -120],
1937     [2, 1, st_ac => "Ac", -120],
1938     [2, 2, st_dam => "Dam", 120],
1939     [2, 3, st_arm => "Arm", 120],
1940     [2, 4, st_spd => "Spd", 10.54],
1941     [2, 5, st_wspd => "WSp", 10.54],
1942     ) {
1943     my ($col, $row, $id, $label, $template) = @$_;
1944    
1945     $tbl->add ($col , $row, $STATWIDS->{$id} = new CFPlus::UI::Label
1946     font => $FONT_FIXED, can_hover => 1, can_events => 1, valign => 0,
1947     align => +1, template => $template, tooltip => "#stat_$label");
1948     $tbl->add ($col + 1, $row, $STATWIDS->{"$id\_lbl"} = new CFPlus::UI::Label
1949     font => $FONT_FIXED, can_hover => 1, can_events => 1, fg => $color2, valign => 0,
1950     align => -1, text => $label, tooltip => "#stat_$label");
1951     }
1952    
1953     $vb->add (new CFPlus::UI::FancyFrame
1954     label => "Resistancies",
1955     child => (my $tbl2 = new CFPlus::UI::Table expand => 1),
1956     );
1957    
1958     my $row = 0;
1959     my $col = 0;
1960    
1961     my %resist_names = (
1962     slow => ["Slow",
1963     "<b>Slow</b> (slows you down when you are hit by the spell. Monsters will have an opportunity to come near you faster and hit you more often.)"],
1964     holyw => ["Holy Word",
1965     "<b>Holy Word</b> (resistance you against getting the fear when someone whose god doesn't like you spells the holy word on you.)"],
1966     conf => ["Confusion",
1967     "<b>Confusion</b> (If you are hit by confusion you will move into random directions, and likely into monsters.)"],
1968     fire => ["Fire",
1969     "<b>Fire</b> (just your resistance to fire spells like burning hands, dragonbreath, meteor swarm fire, ...)"],
1970     depl => ["Depletion",
1971     "<b>Depletion</b> (some monsters and other effects can cause stats depletion)"],
1972     magic => ["Magic",
1973     "<b>Magic</b> (resistance to magic spells like magic missile or similar)"],
1974     drain => ["Draining",
1975     "<b>Draining</b> (some monsters (e.g. vampires) and other effects can steal experience)"],
1976     acid => ["Acid",
1977     "<b>Acid</b> (resistance to acid, acid hurts pretty much and also corrodes your weapons)"],
1978     pois => ["Poison",
1979     "<b>Poison</b> (resistance to getting poisoned)"],
1980     para => ["Paralysation",
1981     "<b>Paralysation</b> (this resistance affects the chance you get paralysed)"],
1982     deat => ["Death",
1983     "<b>Death</b> (resistance against death spells)"],
1984     phys => ["Physical",
1985     "<b>Physical</b> (this is the resistance against physical attacks, like when a monster hit you in melee combat. The value displayed here is also displayed in the 'Arm' field on the left.)"],
1986     blind => ["Blind",
1987     "<b>Blind</b> (blind resistance affects the chance of a successful blinding attack)"],
1988     fear => ["Fear",
1989     "<b>Fear</b> (this attack will drive you away from monsters who cast this and hit you successfully, being resistant to this helps a lot when fighting those monsters)"],
1990     tund => ["Turn undead",
1991     "<b>Turn undead</b> (affects your resistancy to various forms of 'turn undead' spells. Only relevant when you are, in fact, undead..."],
1992     elec => ["Electricity",
1993     "<b>Electricity</b> (resistance against electricity, spells like large lightning, small lightning, ...)"],
1994     cold => ["Cold",
1995     "<b>Cold</b> (this is your resistance against cold spells like icestorm, snowstorm, ...)"],
1996     ghit => ["Ghost hit",
1997     "<b>Ghost hit</b> (special attack used by ghosts and ghost-like beings)"],
1998     );
1999     for (qw/slow holyw conf fire depl magic
2000     drain acid pois para deat phys
2001     blind fear tund elec cold ghit/)
2002     {
2003     $tbl2->add ($col, $row,
2004     $STATWIDS->{"res_$_"} =
2005     new CFPlus::UI::Label
2006     font => $FONT_FIXED,
2007     template => "-100%",
2008     align => +1,
2009     valign => 0,
2010     can_events => 1,
2011     can_hover => 1,
2012     tooltip => $resist_names{$_}->[1],
2013     );
2014     $tbl2->add ($col + 1, $row, new CFPlus::UI::Image
2015     font => $FONT_FIXED,
2016     can_hover => 1,
2017     can_events => 1,
2018     path => "ui/resist/resist_$_.png",
2019     tooltip => $resist_names{$_}->[1],
2020     );
2021     $tbl2->add ($col + 2, $row, new CFPlus::UI::Label
2022     text => $resist_names{$_}->[0],
2023     font => $FONT_FIXED,
2024     can_hover => 1,
2025     can_events => 1,
2026     tooltip => $resist_names{$_}->[1],
2027     );
2028    
2029     $row++;
2030     if ($row % 6 == 0) {
2031     $col += 3;
2032     $row = 0;
2033     }
2034     }
2035    
2036     #update_stats_window ({});
2037    
2038     $r
2039     }
2040    
2041     sub skill_window {
2042     my $sw = new CFPlus::UI::ScrolledWindow (expand => 1);
2043     $sw->add ($STATWIDS->{skill_tbl} = new CFPlus::UI::Table expand => 1, col_expand => [0, 0, 1, 0, 0, 1]);
2044     $sw
2045     }
2046    
2047     sub formsep($) {
2048     scalar reverse join ",", unpack "(A3)*", reverse $_[0] * 1
2049     }
2050    
2051     my $METASERVER_ATIME;
2052    
2053     sub update_metaserver {
2054     my ($metaserver_dialog) = @_;
2055    
2056     $METASERVER = $metaserver_dialog
2057     if defined $metaserver_dialog;
2058    
2059     return if $METASERVER_ATIME > time;
2060     $METASERVER_ATIME = time + 60;
2061    
2062     my $table = $METASERVER->{table};
2063     $table->clear;
2064     $table->add_at (0, 0, my $label = new CFPlus::UI::Label max_w => $WIDTH * 0.8, text => "fetching server list...");
2065    
2066     my $ok = 0;
2067    
2068     CFPlus::background {
2069     my $ua = CFPlus::lwp_useragent;
2070    
2071     CFPlus::background_msg CFPlus::from_json +(CFPlus::lwp_check $ua->get ($META_SERVER))->decoded_content;
2072     } sub {
2073     my ($msg) = @_;
2074     if ($msg) {
2075     $table->clear;
2076    
2077     my @tip = (
2078     "The current number of users logged in on the server.",
2079     "The hostname of the server.",
2080     "The time this server has been running without being restarted.",
2081     "The server software version - a '+' indicates a Crossfire+ server.",
2082     "Short information about this server provided by its admins.",
2083     );
2084     my @col = qw(#Users Host Uptime Version Description);
2085     $table->add_at ($_, 0, new CFPlus::UI::Label
2086     can_hover => 1, can_events => 1,
2087     align => 0, fg => [1, 1, 0],
2088     text => $col[$_], tooltip => $tip[$_])
2089     for 0 .. $#col;
2090    
2091     my @align = qw(1 0 1 1 -1);
2092    
2093     my $y = 0;
2094     for my $m (@{ $msg->{servers} }) {
2095     my ($ip, $last, $host, $users, $version, $desc, $ibytes, $obytes, $uptime, $highlight) =
2096     @$m{qw(ip age hostname users version description ibytes obytes uptime highlight)};
2097    
2098     for ($desc) {
2099     s/<br>/\n/gi;
2100     s/<li>/\n· /gi;
2101     s/<.*?>//sgi;
2102     s/&amp;/&/g;
2103     s/&lt;/</g;
2104     s/&gt;/>/g;
2105     }
2106    
2107     $uptime = sprintf "%dd %02d:%02d:%02d",
2108     (int $uptime / 86400),
2109     (int $uptime / 3600) % 24,
2110     (int $uptime / 60) % 60,
2111     $uptime % 60;
2112    
2113     $m = [$users, $host, $uptime, $version, $desc];
2114    
2115     $y++;
2116    
2117     $table->add_at (scalar @$m, $y, new CFPlus::UI::VBox children => [
2118     (new CFPlus::UI::Button
2119     text => "Use",
2120     tooltip => "Put this server into the <b>Host:Port</b> field",
2121     on_activate => sub {
2122     $HOST_ENTRY->set_text ($CFG->{profile}{default}{host} = $host);
2123     $METASERVER->hide;
2124     0
2125     },
2126     ),
2127     (new CFPlus::UI::Empty expand => 1),
2128     ]);
2129    
2130     $table->add_at ($_, $y, new CFPlus::UI::Label
2131     max_w => $::WIDTH * 0.4,
2132     ellipsise => 0,
2133     align => $align[$_],
2134     text => $m->[$_],
2135     tooltip => $tip[$_],
2136     fg => ($highlight ? [1, 1, 1] : [.7, .7, .7]),
2137     can_hover => 1,
2138     can_events => 1,
2139     fontsize => 0.8)
2140     for 0 .. $#$m;
2141     }
2142     } else {
2143     $ok or $label->set_text ("error while contacting metaserver");
2144     }
2145     };
2146    
2147     }
2148    
2149     sub metaserver_dialog {
2150     my $vbox = new CFPlus::UI::VBox;
2151     my $table = new CFPlus::UI::Table;
2152     $vbox->add (new CFPlus::UI::ScrolledWindow expand => 1, child => $table);
2153    
2154     my $dialog = new CFPlus::UI::Toplevel
2155     title => "Server List",
2156     name => 'metaserver_dialog',
2157     x => 'center',
2158     y => 'center',
2159     z => 3,
2160     force_w => $::WIDTH * 0.9,
2161     force_h => $::HEIGHT * 0.7,
2162     child => $vbox,
2163     has_close_button => 1,
2164     table => $table,
2165     on_visibility_change => sub {
2166     update_metaserver ($_[0]) if $_[1];
2167     0
2168     },
2169     ;
2170    
2171     $dialog
2172     }
2173    
2174     sub server_setup {
2175     my $vbox = new CFPlus::UI::VBox;
2176    
2177     $vbox->add (new CFPlus::UI::FancyFrame
2178     label => "Connection Settings",
2179     child => (my $table = new CFPlus::UI::Table expand => 1, col_expand => [0, 1]),
2180     );
2181     $table->add_at (0, 2, new CFPlus::UI::Label valign => 0, align => 1, text => "Host:Port");
2182    
2183     {
2184     $table->add_at (1, 2, my $vbox = new CFPlus::UI::VBox);
2185    
2186     $vbox->add (
2187     $HOST_ENTRY = new CFPlus::UI::Entry
2188     expand => 1,
2189     text => $CFG->{profile}{default}{host},
2190     tooltip => "The hostname or ip address of the Crossfire(+) server to connect to",
2191     on_changed => sub {
2192     my ($self, $value) = @_;
2193     $CFG->{profile}{default}{host} = $value;
2194     0
2195     }
2196     );
2197    
2198     $vbox->add (new CFPlus::UI::Button
2199     expand => 1,
2200     text => "Server List",
2201     other => $METASERVER,
2202     tooltip => "Show a list of available crossfire servers",
2203     on_activate => sub { $METASERVER->toggle_visibility; 0 },
2204     on_visibility_change => sub { $METASERVER->hide unless $_[1]; 0 },
2205     );
2206     }
2207    
2208     $table->add_at (0, 4, new CFPlus::UI::Label valign => 0, align => 1, text => "Username");
2209     $table->add_at (1, 4, new CFPlus::UI::Entry
2210     text => $CFG->{profile}{default}{user},
2211     tooltip => "The name of your character on the server",
2212     on_changed => sub { my ($self, $value) = @_; $CFG->{profile}{default}{user} = $value }
2213     );
2214    
2215     $table->add_at (0, 5, new CFPlus::UI::Label valign => 0, align => 1, text => "Password");
2216     $table->add_at (1, 5, new CFPlus::UI::Entry
2217     text => $CFG->{profile}{default}{password},
2218     hidden => 1,
2219     tooltip => "The password for your character",
2220     on_changed => sub { my ($self, $value) = @_; $CFG->{profile}{default}{password} = $value }
2221     );
2222    
2223     $table->add_at (0, 7, new CFPlus::UI::Label valign => 0, align => 1, text => "Map Size");
2224     $table->add_at (1, 7, new CFPlus::UI::Slider
2225     force_w => 100,
2226     range => [$CFG->{mapsize}, 10, 100, 0, 1],
2227     tooltip => "This is the size of the portion of the map update the server sends you. "
2228     . "If you set this to a high value you will be able to see further, "
2229     . "but you also increase bandwidth requirements and latency. "
2230     . "This option is only used once at log-in.",
2231     on_changed => sub { my ($self, $value) = @_; $CFG->{mapsize} = $self->{range}[0] = $value = int $value; 0 },
2232     );
2233    
2234     $table->add_at (0, 8, new CFPlus::UI::Label valign => 0, align => 1, text => "Face Prefetch");
2235     $table->add_at (1, 8, new CFPlus::UI::CheckBox
2236     state => $CFG->{face_prefetch},
2237     tooltip => "<b>Background Image Prefetch</b>\n\n"
2238     . "If enabled, the client automatically pre-fetches images from the server. "
2239     . "This might increase or create lag, but increases the chances "
2240     . "of faces being ready for display when you encounter them. "
2241     . "It also uses up server bandwidth on every connect, "
2242     . "so only set it if you really need to prefetch images. "
2243     . "This option can be set and unset any time.",
2244     on_changed => sub { $CFG->{face_prefetch} = $_[1]; 0 },
2245     );
2246    
2247     $table->add_at (0, 9, new CFPlus::UI::Label valign => 0, align => 1, text => "Output-Rate");
2248     $table->add_at (1, 9, new CFPlus::UI::Entry
2249     text => $CFG->{output_rate},
2250     tooltip => "The approximate bandwidth in bytes per second that the server should not exceed "
2251     . "when sending images, to ensure interactiveness. When 0 or unset, the server "
2252     . "default will be used, which is usually around 100kb/s.",
2253     on_changed => sub { $CFG->{output_rate} = $_[1]; 0 },
2254     );
2255    
2256     $table->add_at (0, 10, new CFPlus::UI::Label valign => 0, align => 1, text => "Output-Count");
2257     $table->add_at (1, 10, new CFPlus::UI::Entry
2258     text => $CFG->{output_count},
2259     tooltip => "Should be set to 1 unless you know what you are doing. This option is only used once at log-in.",
2260     on_changed => sub { $CFG->{output_count} = $_[1]; 0 },
2261     );
2262    
2263     $table->add_at (0, 11, new CFPlus::UI::Label valign => 0, align => 1, text => "Output-Sync");
2264     $table->add_at (1, 11, new CFPlus::UI::Entry
2265     text => $CFG->{output_sync},
2266     tooltip => "Should be set to 1 unless you know what you are doing. This option is only used once at log-in.",
2267     on_changed => sub { $CFG->{output_sync} = $_[1]; 0 },
2268     );
2269    
2270     $table->add_at (1, 12, $LOGIN_BUTTON = new CFPlus::UI::Button
2271     expand => 1,
2272     align => 0,
2273     text => "Login",
2274     on_activate => sub {
2275     $CONN ? stop_game
2276     : start_game;
2277     0
2278     },
2279     );
2280    
2281     $vbox->add (new CFPlus::UI::FancyFrame
2282     label => "Server Info",
2283     child => ($SERVER_INFO = new CFPlus::UI::Label ellipsise => 0),
2284     );
2285    
2286     $vbox
2287     }
2288    
2289     sub client_setup {
2290     my $table = new CFPlus::UI::Table expand => 1, col_expand => [0, 1];
2291    
2292     my $row = 0;
2293    
2294     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Chat Command");
2295     $table->add_at (1, $row++, my $saycmd = new CFPlus::UI::Entry
2296     text => $CFG->{say_command},
2297     tooltip => "This is the command that will be used if you write a line in the message window entry or press <b>\"</b> in the map window. "
2298     . "Usually you want to enter something like 'say' or 'shout' or 'gsay' here. "
2299     . "But you could also set it to <b>tell <i>playername</i></b> to only chat with that user.",
2300     on_changed => sub {
2301     my ($self, $value) = @_;
2302     $CFG->{say_command} = $value;
2303     0
2304     }
2305     );
2306    
2307     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Tip of the day");
2308     $table->add_at (1, $row++, new CFPlus::UI::CheckBox
2309     state => $CFG->{show_tips},
2310     tooltip => "Show the <b>Tip of the day</b> window at startup?",
2311     on_changed => sub {
2312     my ($self, $value) = @_;
2313     $CFG->{show_tips} = $value;
2314     0
2315     }
2316     );
2317    
2318     $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Messages Window Size");
2319     $table->add_at (1, $row++, my $saycmd = new CFPlus::UI::Entry
2320     text => $CFG->{logview_max_par},
2321     tooltip => "This is maximum number of messages remembered in the <b>Messages</b> window. If the server "
2322     . "sends more messages than this number, older messages get removed to save memory and "
2323     . "computing time. A value of <b>0</b> disables this feature, but that is not recommended.",
2324     on_changed => sub {
2325     my ($self, $value) = @_;
2326     $LOGVIEW->{max_par} = $CFG->{logview_max_par} = $value*1;
2327     0
2328     },
2329     );
2330    
2331     $table
2332     }
2333    
2334     sub message_window {
2335     my $window = new CFPlus::UI::Toplevel
2336     name => "message_window",
2337     title => "Messages",
2338     border_bg => [1, 1, 1, 1],
2339     x => "max",
2340     y => 0,
2341     force_w => $::WIDTH * 0.4,
2342     force_h => $::HEIGHT * 0.5,
2343     child => (my $vbox = new CFPlus::UI::VBox),
2344     has_close_button => 1;
2345    
2346     $vbox->add ($LOGVIEW);
2347    
2348     $vbox->add (my $input = new CFPlus::UI::Entry
2349     tooltip => "<b>Chat Box</b>. If you enter a text and press return/enter here, the current <i>communication command</i> "
2350     . "from the client setup will be prepended (e.g. <b>shout</b>, <b>chat</b>...). "
2351     . "If you prepend a slash (/), you will submit a command instead (similar to IRC). "
2352     . "A better way to submit commands (and the occasional chat command) is often the map command completer.",
2353     on_focus_in => sub {
2354     my ($input, $prev_focus) = @_;
2355    
2356     delete $input->{refocus_map};
2357    
2358     if ($prev_focus == $MAPWIDGET && $input->{auto_activated}) {
2359     $input->{refocus_map} = 1;
2360     }
2361     delete $input->{auto_activated};
2362    
2363     0
2364     },
2365     on_activate => sub {
2366     my ($input, $text) = @_;
2367     $input->set_text ('');
2368    
2369     if ($text =~ /^\/(.*)/) {
2370     $::CONN->user_send ($1);
2371     } else {
2372     my $say_cmd = $::CFG->{say_command} || 'say';
2373     $::CONN->user_send ("$say_cmd $text");
2374     }
2375     if ($input->{refocus_map}) {
2376     delete $input->{refocus_map};
2377     $MAPWIDGET->focus_in
2378     }
2379    
2380     0
2381     },
2382     on_escape => sub {
2383     $MAPWIDGET->grab_focus;
2384    
2385     0
2386     },
2387     );
2388    
2389     $CONSOLE = {
2390     window => $window,
2391     input => $input,
2392     };
2393    
2394     $window
2395     }
2396    
2397     sub autopickup_setup {
2398     my $r = new CFPlus::UI::ScrolledWindow (
2399     expand => 1,
2400     scroll_y => 1
2401     );
2402     $r->add (my $table = new CFPlus::UI::Table
2403     row_expand => [0],
2404     col_expand => [0, 1, 0, 1],
2405     );
2406    
2407     for (
2408     ["General", 0, 0,
2409     ["Enable autopickup" => PICKUP_NEWMODE, \$PICKUP_ENABLE],
2410     ["Inhibit autopickup" => PICKUP_INHIBIT],
2411     ["Stop before pickup" => PICKUP_STOP],
2412     ["Debug autopickup" => PICKUP_DEBUG],
2413     ],
2414     ["Weapons", 0, 6,
2415     ["All weapons" => PICKUP_ALLWEAPON],
2416     ["Missile weapons" => PICKUP_MISSILEWEAPON],
2417     ["Bows" => PICKUP_BOW],
2418     ["Arrows" => PICKUP_ARROW],
2419     ],
2420     ["Armour", 0, 12,
2421     ["Helmets" => PICKUP_HELMET],
2422     ["Shields" => PICKUP_SHIELD],
2423     ["Body Armour" => PICKUP_ARMOUR],
2424     ["Boots" => PICKUP_BOOTS],
2425     ["Gloves" => PICKUP_GLOVES],
2426     ["Cloaks" => PICKUP_CLOAK],
2427     ],
2428    
2429     ["Readables", 2, 0,
2430     ["Spellbooks" => PICKUP_SPELLBOOK],
2431     ["Skillscrolls" => PICKUP_SKILLSCROLL],
2432     ["Normal Books/Scrolls" => PICKUP_READABLES],
2433     ],
2434     ["Misc", 2, 5,
2435     ["Food" => PICKUP_FOOD],
2436     ["Drinks" => PICKUP_DRINK],
2437     ["Valuables (Money, Gems)" => PICKUP_VALUABLES],
2438     ["Keys" => PICKUP_KEY],
2439     ["Magical Items" => PICKUP_MAGICAL],
2440     ["Potions" => PICKUP_POTION],
2441     ["Magic Devices" => PICKUP_MAGIC_DEVICE],
2442     ["Ignore cursed" => PICKUP_NOT_CURSED],
2443     ["Jewelery" => PICKUP_JEWELS],
2444     ["Flesh" => PICKUP_FLESH],
2445     ],
2446     ["Weight/Value ratio", 2, 17]
2447     )
2448     {
2449     my ($title, $x, $y, @bits) = @$_;
2450     $table->add_at ($x, $y, new CFPlus::UI::Label text => $title, align => 1, fg => [1, 1, 0]);
2451    
2452     for (@bits) {
2453     ++$y;
2454    
2455     my $mask = $_->[1];
2456     $table->add_at ($x , $y, new CFPlus::UI::Label text => $_->[0], align => 1, expand => 1);
2457     $table->add_at ($x+1, $y, my $checkbox = new CFPlus::UI::CheckBox
2458     state => $::CFG->{pickup} & $mask,
2459     on_changed => sub {
2460     my ($box, $value) = @_;
2461    
2462     if ($value) {
2463     $::CFG->{pickup} |= $mask;
2464     } else {
2465     $::CFG->{pickup} &= ~$mask;
2466     }
2467    
2468     $::CONN->send_command ("pickup $::CFG->{pickup}")
2469     if defined $::CONN;
2470    
2471     0
2472     });
2473    
2474     ${$_->[2]} = $checkbox if $_->[2];
2475     }
2476     }
2477    
2478     $table->add_at (2, 18, new CFPlus::UI::ValSlider
2479     range => [$::CFG->{pickup} & 0xF, 0, 16, 1, 1],
2480     template => ">= 99",
2481     to_value => sub { ">= " . 5 * $_[0] },
2482     on_changed => sub {
2483     my ($slider, $value) = @_;
2484    
2485     $::CFG->{pickup} &= ~0xF;
2486     $::CFG->{pickup} |= int $value
2487     if $value;
2488     1;
2489     });
2490    
2491     $table->add_at (3, 18, new CFPlus::UI::Button
2492     text => "set",
2493     on_activate => sub {
2494     $::CONN->send_command ("pickup $::CFG->{pickup}")
2495     if defined $::CONN;
2496     0
2497     });
2498    
2499     $r
2500     }
2501    
2502     my %SORT_ORDER = (
2503     type => undef,
2504     mtime => sub {
2505     my $NOW = time;
2506     sort {
2507     my $atime = $a->{mtime} - $NOW; $atime = $atime < 5 * 60 ? int $atime / 60 : 6;
2508     my $btime = $b->{mtime} - $NOW; $btime = $btime < 5 * 60 ? int $btime / 60 : 6;
2509    
2510     ($a->{flags} & F_LOCKED) <=> ($b->{flags} & F_LOCKED)
2511     or $btime <=> $atime
2512     or $a->{type} <=> $b->{type}
2513     } @_
2514     },
2515     weight => sub { sort {
2516     $a->{weight} * ($a->{nrof} || 1) <=> $b->{weight} * ($b->{nrof} || 1)
2517     or $a->{type} <=> $b->{type}
2518     } @_ },
2519     );
2520    
2521     sub inventory_widget {
2522     my $hb = new CFPlus::UI::HBox homogeneous => 1;
2523    
2524     $hb->add (my $vb1 = new CFPlus::UI::VBox);
2525     $vb1->add (new CFPlus::UI::Label align => 0, text => "Player");
2526    
2527     $vb1->add (my $hb1 = new CFPlus::UI::HBox);
2528    
2529     use sort 'stable';
2530    
2531     $hb1->add (new CFPlus::UI::Selector
2532     value => $::CFG->{inv_sort},
2533     options => [
2534     [type => "Type/Name"],
2535     [mtime => "Recent/Normal/Locked"],
2536     [weight => "Weight/Type"],
2537     ],
2538     on_changed => sub {
2539     $::CFG->{inv_sort} = $_[1];
2540     $INV->set_sort_order ($SORT_ORDER{$_[1]});
2541     },
2542     );
2543     $hb1->add (new CFPlus::UI::Label text => "Weight: ", align => 1, expand => 1);
2544     #TODO# update to weigh/maxweight
2545     $hb1->add ($STATWIDS->{i_weight} = new CFPlus::UI::Label align => -1);
2546    
2547     $vb1->add (my $sw1 = new CFPlus::UI::ScrolledWindow expand => 1, scroll_y => 1);
2548     $sw1->add ($INV = new CFPlus::UI::Inventory);
2549     $INV->set_sort_order ($SORT_ORDER{$::CFG->{inv_sort}});
2550    
2551     $hb->add (my $vb2 = new CFPlus::UI::VBox);
2552    
2553     $vb2->add ($INV_RIGHT_HB = new CFPlus::UI::HBox);
2554    
2555     $vb2->add (my $sw2 = new CFPlus::UI::ScrolledWindow expand => 1, scroll_y => 1);
2556     $sw2->add ($INVR = new CFPlus::UI::Inventory);
2557    
2558     # XXX: Call after $INVR = ... because set_opencont sets the items
2559     CFPlus::Protocol::set_opencont ($::CONN, 0, "Floor");
2560    
2561     $hb
2562     }
2563    
2564     sub toggle_player_page {
2565     my ($widget) = @_;
2566    
2567     if ($PL_WINDOW->{visible} && $PL_NOTEBOOK->get_current_page == $widget) {
2568     $PL_WINDOW->hide;
2569     } else {
2570     $PL_NOTEBOOK->set_current_page ($widget);
2571     $PL_WINDOW->show;
2572     }
2573     }
2574    
2575     sub player_window {
2576     my $plwin = $PL_WINDOW = new CFPlus::UI::Toplevel
2577     x => "center",
2578     y => "center",
2579     force_w => $WIDTH * 9/10,
2580     force_h => $HEIGHT * 9/10,
2581     title => "Player",
2582     name => "playerbook",
2583     has_close_button => 1
2584     ;
2585    
2586     my $ntb =
2587     $PL_NOTEBOOK =
2588     new CFPlus::UI::Notebook expand => 1;
2589    
2590     $ntb->add (
2591     "Statistics (F2)" => $STATS_PAGE = stats_window,
2592     "Shows statistics, where all your Stats and Resistances are shown."
2593     );
2594     $ntb->add (
2595     "Skills (F3)" => $SKILL_PAGE = skill_window,
2596     "Shows all your Skills."
2597     );
2598    
2599     my $spellsw = $SPELL_PAGE = new CFPlus::UI::ScrolledWindow (expand => 1, scroll_y => 1);
2600     $spellsw->add ($SPELL_LIST = new CFPlus::UI::SpellList);
2601     $ntb->add (
2602     "Spellbook (F4)" => $spellsw,
2603     "Displays all spells you have and lets you edit keyboard shortcuts for them."
2604     );
2605     $ntb->add (
2606     "Inventory (F5)" => $INVENTORY_PAGE = inventory_widget,
2607     "Toggles the inventory window, where you can manage your loot (or treasures :). "
2608     . "You can also hit the <b>Tab</b>-key to show/hide the Inventory."
2609     );
2610     $ntb->add (Pickup => autopickup_setup,
2611     "Configure autopickup settings, i.e. which items you will pick up automatically when walking (or running) over them.");
2612    
2613     $ntb->set_current_page ($INVENTORY_PAGE);
2614    
2615     $plwin->add ($ntb);
2616     $plwin
2617     }
2618    
2619     sub keyboard_setup {
2620     CFPlus::Macro::keyboard_setup
2621     }
2622    
2623     sub help_window {
2624     my $win = new CFPlus::UI::Toplevel
2625     x => 'center',
2626     y => 'center',
2627     z => 4,
2628     name => 'doc_browser',
2629     force_w => int $WIDTH * 7/8,
2630     force_h => int $HEIGHT * 7/8,
2631     title => "Help Browser",
2632     has_close_button => 1;
2633    
2634     $win->add (my $vbox = new CFPlus::UI::VBox);
2635    
2636     $vbox->add (new CFPlus::UI::FancyFrame
2637     label => "Navigation",
2638     child => (my $buttons = new CFPlus::UI::HBox),
2639     );
2640     $vbox->add (my $viewer = new CFPlus::UI::TextScroller
2641     expand => 1, fontsize => 0.8, padding_x => 4, padding_y => 4);
2642    
2643     my @history;
2644     my @future;
2645     my $curnode;
2646    
2647     my $load_node; $load_node = sub {
2648     my ($node, $para) = @_;
2649    
2650     $buttons->clear;
2651    
2652     $buttons->add (new CFPlus::UI::Button
2653     text => "⇤",
2654     tooltip => "back to the starting page",
2655     on_activate => sub {
2656     unshift @future, [$curnode, $viewer->current_paragraph] if $curnode;
2657     unshift @future, @history;
2658     @history = ();
2659     $load_node->(@{shift @future});
2660     },
2661     );
2662    
2663     if (@history) {
2664     $buttons->add (new CFPlus::UI::Button
2665     text => "⋘",
2666     tooltip => "back to <i>" . (CFPlus::asxml CFPlus::Pod::full_path $history[-1][0]) . "</i>",
2667     on_activate => sub {
2668     unshift @future, [$curnode, $viewer->current_paragraph] if $curnode;
2669     $load_node->(@{pop @history});
2670     },
2671     );
2672     }
2673    
2674     if (@future) {
2675     $buttons->add (new CFPlus::UI::Button
2676     text => "â‹™",
2677     tooltip => "forward to <i>" . (CFPlus::asxml CFPlus::Pod::full_path $future[0][0]) . "</i>",
2678     on_activate => sub {
2679     push @history, [$curnode, $viewer->current_paragraph];
2680     $load_node->(@{shift @future});
2681     },
2682     );
2683     }
2684    
2685     $buttons->add (new CFPlus::UI::Label text => " ");
2686    
2687     my @path = CFPlus::Pod::full_path_of $node;
2688     pop @path; # drop current node
2689    
2690     for my $node (@path) {
2691     $buttons->add (new CFPlus::UI::Button
2692     text => $node->{kw}[0],
2693     tooltip => "go to <i>" . (CFPlus::asxml CFPlus::Pod::full_path $node) . "</i>",
2694     on_activate => sub {
2695     push @history, [$curnode, $viewer->current_paragraph] if $curnode; @future = ();
2696     $load_node->($node);
2697     },
2698     );
2699     $buttons->add (new CFPlus::UI::Label text => "/");
2700     }
2701    
2702     $buttons->add (new CFPlus::UI::Label text => $node->{kw}[0], padding_x => 4, padding_y => 4);
2703    
2704     $curnode = $node;
2705    
2706     $viewer->clear;
2707     $viewer->add_paragraph (CFPlus::Pod::as_paragraphs CFPlus::Pod::section_of $curnode);
2708     $viewer->scroll_to ($para);
2709     };
2710    
2711     $load_node->(CFPlus::Pod::find pod => "mainpage");
2712    
2713     $CFPlus::Pod::goto_document = sub {
2714     my (@path) = @_;
2715    
2716     push @history, [$curnode, $viewer->current_paragraph] if $curnode; @future = ();
2717    
2718     $load_node->((CFPlus::Pod::find @path)[0]);
2719     $win->show;
2720     };
2721    
2722     $win
2723     }
2724    
2725     sub open_string_query {
2726     my ($title, $cb, $txt, $tooltip) = @_;
2727     my $dialog = new CFPlus::UI::Toplevel
2728     x => "center",
2729     y => "center",
2730     z => 50,
2731     force_w => $WIDTH * 4/5,
2732     title => $title;
2733    
2734     $dialog->add (
2735     my $e = new CFPlus::UI::Entry
2736     on_activate => sub { $cb->(@_); $dialog->hide; 0 },
2737     on_key_down => sub { $_[1]->{sym} == 27 and $dialog->hide; 0 },
2738     tooltip => $tooltip
2739     );
2740    
2741     $e->grab_focus;
2742     $e->set_text ($txt) if $txt;
2743     $dialog->show;
2744     }
2745    
2746     sub open_quit_dialog {
2747     unless ($QUIT_DIALOG) {
2748     $QUIT_DIALOG = new CFPlus::UI::Toplevel
2749     x => "center",
2750     y => "center",
2751     z => 50,
2752     title => "Really Quit?",
2753     on_key_down => sub {
2754     my ($dialog, $ev) = @_;
2755     $ev->{sym} == 27 and $dialog->hide;
2756     }
2757     ;
2758    
2759     $QUIT_DIALOG->add (my $vb = new CFPlus::UI::VBox expand => 1);
2760    
2761     $vb->add (new CFPlus::UI::Label
2762     text => "You should find a savebed and apply it first!",
2763     max_w => $WIDTH * 0.25,
2764     ellipsize => 0,
2765     );
2766     $vb->add (my $hb = new CFPlus::UI::HBox expand => 1);
2767     $hb->add (new CFPlus::UI::Button
2768     text => "Ok",
2769     expand => 1,
2770     on_activate => sub { $QUIT_DIALOG->hide; 0 },
2771     );
2772     $hb->add (new CFPlus::UI::Button
2773     text => "Quit anyway",
2774     expand => 1,
2775     on_activate => sub { exit },
2776     );
2777     }
2778    
2779     $QUIT_DIALOG->show;
2780     $QUIT_DIALOG->grab_focus;
2781     }
2782    
2783     sub show_tip_of_the_day {
2784     # find all tips
2785     my @tod = CFPlus::Pod::find tip_of_the_day => "*";
2786    
2787     CFPlus::DB::get state => "tip_of_the_day", sub {
2788     my ($todindex) = @_;
2789     $todindex = 0 if $todindex >= @tod;
2790     CFPlus::DB::put state => tip_of_the_day => $todindex + 1, sub { };
2791    
2792     # create dialog
2793     my $dialog;
2794    
2795     my $close = sub {
2796     $dialog->destroy;
2797     };
2798    
2799     $dialog = new CFPlus::UI::Toplevel
2800     x => "center",
2801     y => "center",
2802     z => 3,
2803     name => 'tip_of_the_day',
2804     force_w => int $WIDTH * 4/9,
2805     force_h => int $WIDTH * 2/9,
2806     title => "Tip of the day #" . (1 + $todindex),
2807     child => my $vbox = new CFPlus::UI::VBox,
2808     has_close_button => 1,
2809     on_delete => $close,
2810     ;
2811    
2812     $vbox->add (my $viewer = new CFPlus::UI::TextScroller
2813     expand => 1, fontsize => 0.8, padding_x => 4, padding_y => 4);
2814     $viewer->add_paragraph (CFPlus::Pod::as_paragraphs CFPlus::Pod::section_of $tod[$todindex]);
2815    
2816     $vbox->add (my $table = new CFPlus::UI::Table col_expand => [0, 1]);
2817    
2818     $table->add_at (0, 0, new CFPlus::UI::Button
2819     text => "Close",
2820     tooltip => "Close the tip of the day window. To never see it again, disable the tip of the day in the <b>Server Setup</b>.",
2821     on_activate => $close,
2822     );
2823    
2824     $table->add_at (2, 0, new CFPlus::UI::Button
2825     text => "Next",
2826     tooltip => "Show the next <b>Tip of the day</b>.",
2827     on_activate => sub {
2828     $close->();
2829     &show_tip_of_the_day;
2830     },
2831     );
2832    
2833     $dialog->show;
2834     };
2835     }
2836    
2837     sub sdl_init {
2838     CFPlus::SDL_Init
2839     and die "SDL::Init failed!\n";
2840     }
2841    
2842     sub video_init {
2843     $CFG->{sdl_mode} = 0 if $CFG->{sdl_mode} >= @SDL_MODES;
2844    
2845     my ($old_w, $old_h) = ($WIDTH, $HEIGHT);
2846    
2847     ($WIDTH, $HEIGHT, my ($rgb, $alpha)) = @{ $SDL_MODES[$CFG->{sdl_mode}] };
2848     $FULLSCREEN = $CFG->{fullscreen};
2849     $FAST = $CFG->{fast};
2850    
2851     CFPlus::SDL_SetVideoMode $WIDTH, $HEIGHT, $rgb, $alpha, $FULLSCREEN
2852     or die "SDL_SetVideoMode failed: " . (CFPlus::SDL_GetError) . "\n";
2853    
2854     $SDL_ACTIVE = 1;
2855     $LAST_REFRESH = time - 0.01;
2856    
2857     CFPlus::OpenGL::init;
2858    
2859     $FONTSIZE = int $HEIGHT / 40 * $CFG->{gui_fontsize};
2860    
2861     $CFPlus::UI::ROOT->configure (0, 0, $WIDTH, $HEIGHT);#d#
2862    
2863     #############################################################################
2864    
2865     if ($DEBUG_STATUS) {
2866     CFPlus::UI::rescale_widgets $WIDTH / $old_w, $HEIGHT / $old_h;
2867     } else {
2868     # create the widgets
2869    
2870     $DEBUG_STATUS = new CFPlus::UI::Label
2871     padding => 0,
2872     z => 100,
2873     force_x => "max",
2874     force_y => 0;
2875     $DEBUG_STATUS->show;
2876    
2877     $STATUSBOX = new CFPlus::UI::Statusbox;
2878     $STATUSBOX->add ("Use <b>Alt-Enter</b> to toggle fullscreen mode", timeout => 864000, pri => -100, color => [1, 1, 1, 0.8]);
2879    
2880     (new CFPlus::UI::Frame
2881     bg => [0, 0, 0, 0.4],
2882     force_x => 0,
2883     force_y => "max",
2884     child => $STATUSBOX,
2885     )->show;
2886    
2887     CFPlus::UI::Toplevel->new (
2888     title => "Map",
2889     name => "mapmap",
2890     x => 0,
2891     y => $FONTSIZE + 8,
2892     border_bg => [1, 1, 1, 192/255],
2893     bg => [1, 1, 1, 0],
2894     child => ($MAPMAP = new CFPlus::MapWidget::MapMap
2895     tooltip => "<b>Map</b>. On servers that support this feature, this will display an overview of the surrounding areas.",
2896     ),
2897     )->show;
2898    
2899     $MAPWIDGET = new CFPlus::MapWidget;
2900     $MAPWIDGET->connect (activate_console => sub {
2901     my ($mapwidget, $preset) = @_;
2902    
2903     if ($CONSOLE) {
2904     $CONSOLE->{input}->{auto_activated} = 1;
2905     $CONSOLE->{input}->grab_focus;
2906    
2907     if ($preset && $CONSOLE->{input}->get_text eq '') {
2908     $CONSOLE->{input}->set_text ($preset);
2909     }
2910     }
2911     });
2912     $MAPWIDGET->show;
2913     $MAPWIDGET->grab_focus;
2914    
2915     $LOGVIEW = new CFPlus::UI::TextScroller
2916     expand => 1,
2917     font => $FONT_FIXED,
2918     fontsize => $::CFG->{log_fontsize},
2919     indent => -4,
2920     can_hover => 1,
2921     can_events => 1,
2922     max_par => $CFG->{logview_max_par},
2923     tooltip => "<b>Server Log</b>. This text viewer contains all recent messages sent by the server.",
2924     ;
2925    
2926     $SETUP_DIALOG = new CFPlus::UI::Toplevel
2927     title => "Setup",
2928     name => "setup_dialog",
2929     x => 'center',
2930     y => 'center',
2931     z => 2,
2932     force_w => $::WIDTH * 0.6,
2933     force_h => $::HEIGHT * 0.6,
2934     has_close_button => 1,
2935     ;
2936    
2937     $METASERVER = metaserver_dialog;
2938    
2939     $SETUP_DIALOG->add ($SETUP_NOTEBOOK = new CFPlus::UI::Notebook expand => 1, debug => 1,
2940     filter => new CFPlus::UI::ScrolledWindow expand => 1, scroll_y => 1);
2941    
2942     $SETUP_NOTEBOOK->add (Server => $SETUP_SERVER = server_setup,
2943     "Configure the server to play on, your username, password and other server-related options.");
2944     $SETUP_NOTEBOOK->add (Client => client_setup,
2945     "Configure various client-specific settings.");
2946     $SETUP_NOTEBOOK->add (Graphics => graphics_setup,
2947     "Configure the video mode, performance, fonts and other graphical aspects of the game.");
2948     $SETUP_NOTEBOOK->add (Audio => audio_setup,
2949     "Configure the use of audio, sound effects and background music.");
2950     $SETUP_NOTEBOOK->add (Keyboard => $SETUP_KEYBOARD = keyboard_setup,
2951     "Lets you define, edit and delete key bindings."
2952     . "There is a shortcut for making bindings: <b>Control-Insert</b> opens the binding editor "
2953     . "with nothing set and the recording started. After doing the actions you "
2954     . "want to record press <b>Insert</b> and you will be asked to press a key-combo. "
2955     . "After pressing the combo the binding will be saved automatically and the "
2956     . "binding editor closes");
2957     $SETUP_NOTEBOOK->add (Debug => debug_setup,
2958     "Some debuggin' options. Do not ask.");
2959    
2960     $BUTTONBAR = new CFPlus::UI::Buttonbar x => 0, y => 0, z => 200; # put on top
2961    
2962     $BUTTONBAR->add (new CFPlus::UI::Flopper text => "Setup", other => $SETUP_DIALOG,
2963     tooltip => "Toggles a dialog where you can configure all aspects of this client.");
2964    
2965     $BUTTONBAR->add (new CFPlus::UI::Flopper text => "Message Window", other => $MESSAGE_WINDOW = message_window,
2966     tooltip => "Toggles the server message log, where the client collects <i>all</i> messages from the server.");
2967    
2968     make_gauge_window->show; # XXX: this has to be set before make_stats_window as make_stats_window calls update_stats_window which updated the gauges also X-D
2969    
2970     $BUTTONBAR->add (new CFPlus::UI::Flopper text => "Playerbook", other => player_window,
2971     tooltip => "Toggles the player view, where you can manage Inventory, Spells, Skills and see your Stats.");
2972    
2973     $BUTTONBAR->add (new CFPlus::UI::Button
2974     text => "Save Config",
2975     tooltip => "Saves the options chosen in the client setting, server settings and the window layout to be restored on later runs.",
2976     on_activate => sub {
2977     $::CFG->{layout} = CFPlus::UI::get_layout;
2978     CFPlus::write_cfg "$Crossfire::VARDIR/cfplusrc";
2979     status "Configuration Saved";
2980     0
2981     },
2982     );
2983    
2984     $BUTTONBAR->add (new CFPlus::UI::Flopper text => "Help!", other => $HELP_WINDOW = help_window,
2985     tooltip => "View Documentation");
2986    
2987    
2988     $BUTTONBAR->add (new CFPlus::UI::Button
2989     text => "Quit",
2990     tooltip => "Terminates the program",
2991     on_activate => sub {
2992     if ($CONN) {
2993     open_quit_dialog;
2994     } else {
2995     exit;
2996     }
2997     0
2998     },
2999     );
3000    
3001     $BUTTONBAR->show;
3002     $SETUP_DIALOG->show;
3003     }
3004    
3005     $STATUSBOX->add ("Set video mode $WIDTH×$HEIGHT", timeout => 10, fg => [1, 1, 1, 0.5]);
3006     }
3007    
3008     sub setup_build_button {
3009     my ($enabled) = @_;
3010     if ($enabled) {
3011     $BUILD_BUTTON->hide if $BUILD_BUTTON;
3012     $BUILD_BUTTON ||= new CFPlus::UI::Button
3013     text => "Build",
3014     tooltip => "Opens the ingame builder",
3015     on_activate => sub {
3016     if ($CONN) {
3017     $CONN->send_ext_req (builder_player_items => sub {
3018     open_ingame_editor ($_[0]) if exists $_[0]->{items};
3019     });
3020     }
3021     0
3022     };
3023     $BUTTONBAR->add ($BUILD_BUTTON);
3024     } else {
3025     $BUILD_BUTTON->hide if $BUILD_BUTTON;
3026     }
3027     }
3028    
3029     sub open_ingame_editor {
3030     my ($msg) = @_;
3031    
3032     my $win = new CFPlus::UI::Toplevel
3033     x => 0,
3034     y => 'center',
3035     z => 4,
3036     name => 'builder_window',
3037     force_w => int $WIDTH * 1/4,
3038     force_h => int $HEIGHT * 3/4,
3039     title => "In game builder",
3040     has_close_button => 1;
3041    
3042     my $r = new CFPlus::UI::ScrolledWindow (
3043     expand => 1,
3044     scroll_y => 1
3045     );
3046     $r->add (my $vb = new CFPlus::UI::VBox);
3047     $win->add ($r);
3048    
3049    
3050     $vb->add (
3051     new CFPlus::UI::Button
3052     text => "Disable build mode",
3053     on_activate => sub { $::IN_BUILD_MODE = undef }
3054     );
3055     $vb->add (
3056     new CFPlus::UI::Button
3057     text => "ERASE",
3058     on_activate => sub { $::IN_BUILD_MODE = { do_erase => 1 } }
3059     );
3060    
3061     for my $itemarchname (
3062     sort {
3063     $msg->{items}->{$a}->{build_arch_name}
3064     cmp $msg->{items}->{$b}->{build_arch_name}
3065     } keys %{$msg->{items}}
3066     ) {
3067     my $info = $msg->{items}->{$itemarchname};
3068     $vb->add (
3069     new CFPlus::UI::Button text => $info->{build_arch_name},
3070     on_activate => sub {
3071     $::IN_BUILD_MODE = { item => $itemarchname, info => $info };
3072    
3073     if (grep { $msg->{items}->{$itemarchname}->{$_} } qw/has_connection has_name has_text/) {
3074     build_mode_query_arch_info ();
3075     }
3076     }
3077     );
3078     }
3079    
3080     $win->show;
3081     }
3082    
3083     sub build_mode_query_arch_info {
3084     my ($iteminfo) = $::IN_BUILD_MODE;
3085     my $itemarchname = $iteminfo->{item};
3086     my $info = $iteminfo->{info};
3087    
3088     my $dialog = new CFPlus::UI::Toplevel
3089     x => "center",
3090     y => "center",
3091     z => 50,
3092     force_w => int $WIDTH * 1/2,
3093     title => "Enter information for placement of '$itemarchname'",
3094     has_close_button => 1;
3095    
3096     $dialog->add (my $vb = new CFPlus::UI::VBox expand => 1);
3097    
3098     $vb->add (my $table = new CFPlus::UI::Table expand => 1);
3099     my $row = 0;
3100     if ($info->{has_name}) {
3101     $table->add_at (0, $row, new CFPlus::UI::Label text => "Name:");
3102     $table->add_at (1, $row++, new CFPlus::UI::Entry expand => 1, on_changed => sub { $::IN_BUILD_MODE->{name} = $_[1]; 0 });
3103     }
3104     if ($info->{has_text}) {
3105     $table->add_at (0, $row, new CFPlus::UI::Label text => "Text:");
3106     $table->add_at (1, $row++, new CFPlus::UI::Entry expand => 1, on_changed => sub { $::IN_BUILD_MODE->{text} = $_[1]; 0 });
3107     }
3108     if ($info->{has_connection}) {
3109     $table->add_at (0, $row, new CFPlus::UI::Label text => "Connection ID:");
3110     $table->add_at (1, $row++,
3111     new CFPlus::UI::Entry
3112     expand => 1,
3113     on_changed => sub { $::IN_BUILD_MODE->{connection} = $_[1]; 0 },
3114     tooltip => "Enter the connection ID here. The connection ID connects actors like a lever to a gate or a magic ear to a gate"
3115     );
3116     }
3117    
3118     $vb->add (my $hb = new CFPlus::UI::HBox expand => 1);
3119     $hb->add (new CFPlus::UI::Button
3120     text => "Close",
3121     expand => 1,
3122     on_activate => sub { $dialog->hide; 0 },
3123     );
3124     $dialog->show;
3125     }
3126    
3127     sub video_shutdown {
3128     CFPlus::OpenGL::shutdown;
3129    
3130     undef $SDL_ACTIVE;
3131     }
3132    
3133     sub audio_channel_finished {
3134     my ($channel) = @_;
3135    
3136     #warn "channel $channel finished\n";#d#
3137     }
3138    
3139     sub audio_music_set {
3140     my ($songs) = @_;
3141    
3142     my @want =
3143     grep $_,
3144     map $CONN->{music_meta}{$_},
3145     @$songs;
3146    
3147     if (@want) {
3148     @MUSIC_WANT = @want;
3149     &audio_music_changed ();
3150     }
3151     }
3152    
3153     sub audio_music_start {
3154     my $path = $MUSIC_PLAYING->{path}
3155     or return;
3156    
3157     CFPlus::DB::prefetch_file $path, 1024_000, sub {
3158     # music might have changed...
3159     $path eq $MUSIC_PLAYING->{path}
3160     or return &audio_music_start ();
3161    
3162     $MUSIC_PLAYER = new_from_file CFPlus::MixMusic $path;
3163    
3164     my $NOW = time;
3165    
3166     if ($MUSIC_PLAYING->{stop_time} > $NOW - $MUSIC_RESUME) {
3167     my $pos = $MUSIC_PLAYING->{stop_pos};
3168     $MUSIC_PLAYER->fade_in_pos (0, 1000, $pos);
3169     $MUSIC_START = time - $pos;
3170     } else {
3171     $MUSIC_PLAYER->play (0);
3172     $MUSIC_START = time;
3173     }
3174    
3175     delete $MUSIC_PLAYING->{stop_time};
3176     delete $MUSIC_PLAYING->{stop_pos};
3177     }
3178     }
3179    
3180     sub audio_music_changed {
3181     return unless $CFG->{bgm_enable};
3182    
3183     # default MUSIC_WANT == MUSIC_DEFAULT
3184     @MUSIC_WANT = { path => CFPlus::find_rcfile "music/$MUSIC_DEFAULT" } unless @MUSIC_WANT;
3185    
3186     # if the currently playing song is acceptable, let it continue
3187     return if $MUSIC_PLAYING
3188     && grep $MUSIC_PLAYING->{path} eq $_->{path}, @MUSIC_WANT;
3189    
3190     my $NOW = time;
3191    
3192     if ($MUSIC_PLAYING) {
3193     $MUSIC_PLAYING->{stop_time} = $NOW;
3194     $MUSIC_PLAYING->{stop_pos} = $NOW - $MUSIC_START;
3195     CFPlus::MixMusic::fade_out 1000;
3196     } else {
3197     # sort by stop time, oldest first
3198     @MUSIC_WANT = sort { $a->{stop_time} <=> $b->{stop_time} } @MUSIC_WANT;
3199    
3200     # if the most recently-played piece played very recently,
3201     # resume it, else choose the oldest piece for rotation.
3202     $MUSIC_PLAYING =
3203     $MUSIC_WANT[-1]{stop_time} > $NOW - $MUSIC_RESUME
3204     ? $MUSIC_WANT[-1]
3205     : $MUSIC_WANT[0];
3206    
3207     audio_music_start;
3208     }
3209     }
3210    
3211     sub audio_music_finished {
3212     $MUSIC_PLAYING = undef;
3213     undef $MUSIC_PLAYER;
3214    
3215     audio_music_changed;
3216     }
3217    
3218     sub audio_init {
3219     if ($CFG->{audio_enable}) {
3220     if (open my $fh, "<", CFPlus::find_rcfile "sounds/config") {
3221     $SDL_MIXER = !CFPlus::Mix_OpenAudio;
3222    
3223     unless ($SDL_MIXER) {
3224     status "Unable to open sound device: there will be no sound";
3225     return;
3226     }
3227    
3228     CFPlus::Mix_AllocateChannels 8;
3229     CFPlus::MixMusic::volume $CFG->{bgm_volume} * 128;
3230    
3231     audio_music_finished;
3232    
3233     local $_;
3234     while (<$fh>) {
3235     next if /^\s*#/;
3236     next if /^\s*$/;
3237    
3238     my ($file, $volume, $event) = split /\s+/, $_, 3;
3239    
3240     push @SOUNDS, "$volume,$file";
3241    
3242     $AUDIO_CHUNKS{"$volume,$file"} ||= do {
3243     my $chunk = new_from_file CFPlus::MixChunk CFPlus::find_rcfile "sounds/$file";
3244     $chunk->volume ($volume * 128 / 100);
3245     $chunk
3246     };
3247     }
3248     } else {
3249     status "unable to open sound config: $!";
3250     }
3251     }
3252     }
3253    
3254     sub audio_shutdown {
3255     CFPlus::Mix_CloseAudio if $SDL_MIXER;
3256     undef $SDL_MIXER;
3257     @SOUNDS = ();
3258     %AUDIO_CHUNKS = ();
3259     }
3260    
3261     my %animate_object;
3262     my $animate_timer;
3263    
3264     my $fps = 9;
3265    
3266     my %demo;#d#
3267    
3268     sub force_refresh {
3269     $fps = $fps * 0.95 + 1 / (($NOW - $LAST_REFRESH) || 0.1) * 0.05;
3270     debug sprintf "%3.2f", $fps if $ENV{CFPLUS_DEBUG} & 4;
3271    
3272     $CFPlus::UI::ROOT->draw;
3273    
3274     $WANT_REFRESH = 0;
3275     $CAN_REFRESH = 0;
3276     $LAST_REFRESH = $NOW;
3277    
3278     CFPlus::SDL_GL_SwapBuffers;
3279     }
3280    
3281     my $refresh_watcher = Event->timer (after => 0, hard => 0, interval => 1 / $MAX_FPS, cb => sub {
3282     $NOW = time;
3283    
3284     ($SDL_CB{$_->{type}} || sub { warn "unhandled event $_->{type}" })->($_)
3285     for CFPlus::poll_events;
3286    
3287     if (%animate_object) {
3288     $_->animate ($LAST_REFRESH - $NOW) for values %animate_object;
3289     ++$WANT_REFRESH;
3290     }
3291    
3292     if ($WANT_REFRESH) {
3293     force_refresh;
3294     } else {
3295     $CAN_REFRESH = 1;
3296     }
3297     });
3298    
3299     sub animation_start {
3300     my ($widget) = @_;
3301     $animate_object{$widget} = $widget;
3302     }
3303    
3304     sub animation_stop {
3305     my ($widget) = @_;
3306     delete $animate_object{$widget};
3307     }
3308    
3309     # check once/second for faces that need to be prefetched
3310     # this should, of course, only run on demand, but
3311     # SDL forces worse things on us....
3312    
3313     Event->timer (after => 1, interval => 0.25, cb => sub {
3314     $CONN->face_prefetch
3315     if $CONN;
3316     });
3317    
3318     %SDL_CB = (
3319     CFPlus::SDL_QUIT => sub {
3320     exit;
3321     },
3322     CFPlus::SDL_VIDEORESIZE => sub {
3323     },
3324     CFPlus::SDL_VIDEOEXPOSE => sub {
3325     CFPlus::UI::full_refresh;
3326     },
3327     CFPlus::SDL_ACTIVEEVENT => sub {
3328     # not useful, as APPACTIVE include sonly iconified state, not unmapped
3329     # printf "active %x %x %x\n", $_[0]{gain}, $_[0]{state}, CFPlus::SDL_GetAppState;#d#
3330     # printf "a %x\n", CFPlus::SDL_GetAppState & CFPlus::SDL_APPACTIVE;#d#
3331     # printf "A\n" if $_[0]{state} & CFPlus::SDL_APPACTIVE;
3332     # printf "K\n" if $_[0]{state} & CFPlus::SDL_APPINPUTFOCUS;
3333     # printf "M\n" if $_[0]{state} & CFPlus::SDL_APPMOUSEFOCUS;
3334     },
3335     CFPlus::SDL_KEYDOWN => sub {
3336     if ($_[0]{mod} & CFPlus::KMOD_ALT && $_[0]{sym} == 13) {
3337     # alt-enter
3338     $FULLSCREEN_ENABLE->toggle;
3339     video_shutdown;
3340     video_init;
3341     } else {
3342     CFPlus::UI::feed_sdl_key_down_event ($_[0]);
3343     }
3344     },
3345     CFPlus::SDL_KEYUP => \&CFPlus::UI::feed_sdl_key_up_event,
3346     CFPlus::SDL_MOUSEMOTION => \&CFPlus::UI::feed_sdl_motion_event,
3347     CFPlus::SDL_MOUSEBUTTONDOWN => \&CFPlus::UI::feed_sdl_button_down_event,
3348     CFPlus::SDL_MOUSEBUTTONUP => \&CFPlus::UI::feed_sdl_button_up_event,
3349     CFPlus::SDL_USEREVENT => sub {
3350     if ($_[0]{code} == 1) {
3351     audio_channel_finished $_[0]{data1};
3352     } elsif ($_[0]{code} == 0) {
3353     audio_music_finished;
3354     }
3355     },
3356     );
3357    
3358     #############################################################################
3359    
3360     $SIG{INT} = $SIG{TERM} = sub { exit };
3361    
3362     {
3363     CFPlus::read_cfg "$Crossfire::VARDIR/cfplusrc";
3364     CFPlus::DB::Server::run;
3365    
3366     CFPlus::UI::set_layout ($::CFG->{layout});
3367    
3368     my %DEF_CFG = (
3369     sdl_mode => 0,
3370     width => 640,
3371     height => 480,
3372     fullscreen => 0,
3373     fast => 0,
3374     map_scale => 1,
3375     fow_enable => 1,
3376     fow_intensity => 0,
3377     map_smoothing => 1,
3378     gui_fontsize => 1,
3379     log_fontsize => 0.7,
3380     gauge_fontsize => 1,
3381     gauge_size => 0.35,
3382     stat_fontsize => 0.7,
3383     mapsize => 100,
3384     say_command => 'chat',
3385     audio_enable => 1,
3386     bgm_enable => 1,
3387     bgm_volume => 0.25,
3388     face_prefetch => 0,
3389     output_sync => 1,
3390     output_count => 1,
3391     output_rate => "",
3392     pickup => 0,
3393     inv_sort => "mtime",
3394     default => "profile", # default profile
3395     show_tips => 1,
3396     logview_max_par => 1000,
3397     );
3398    
3399     while (my ($k, $v) = each %DEF_CFG) {
3400     $CFG->{$k} = $v unless exists $CFG->{$k};
3401     }
3402    
3403     $CFG->{profile}{default}{host} ||= "crossfire.schmorp.de";
3404     $PROFILE = $CFG->{profile}{default};
3405    
3406     # convert old bindings (only default profile matters)
3407     if (my $bindings = delete $PROFILE->{bindings}) {
3408     while (my ($mod, $syms) = each %$bindings) {
3409     while (my ($sym, $cmds) = each %$syms) {
3410     push @{ $PROFILE->{macro} }, {
3411     accelkey => [$mod*1, $sym*1],
3412     action => $cmds,
3413     };
3414     }
3415     }
3416     }
3417    
3418     sdl_init;
3419    
3420     @SDL_MODES = CFPlus::SDL_ListModes 8, 8;
3421     @SDL_MODES = CFPlus::SDL_ListModes 5, 0 unless @SDL_MODES;
3422     @SDL_MODES or CFPlus::fatal "Unable to find a usable video mode\n(hardware accelerated opengl fullscreen)";
3423    
3424     @SDL_MODES = sort { $a->[0] * $a->[1] <=> $b->[0] * $b->[1] } @SDL_MODES;
3425    
3426     $CFG->{sdl_mode} = 0 if $CFG->{sdl_mode} > @SDL_MODES;
3427    
3428     {
3429     my @fonts = map CFPlus::find_rcfile "fonts/$_", qw(
3430     DejaVuSans.ttf
3431     DejaVuSansMono.ttf
3432     DejaVuSans-Bold.ttf
3433     DejaVuSansMono-Bold.ttf
3434     DejaVuSans-Oblique.ttf
3435     DejaVuSansMono-Oblique.ttf
3436     DejaVuSans-BoldOblique.ttf
3437     DejaVuSansMono-BoldOblique.ttf
3438     );
3439    
3440     CFPlus::add_font $_ for @fonts;
3441    
3442     CFPlus::pango_init;
3443    
3444     $FONT_PROP = new_from_file CFPlus::Font $fonts[0];
3445     $FONT_FIXED = new_from_file CFPlus::Font $fonts[1];
3446    
3447     $FONT_PROP->make_default;
3448     }
3449    
3450     # compare mono (ft) vs. rgba (cairo)
3451     # ft - 1.8s, cairo 3s, even in alpha-only mode
3452     # for my $rgba (0..1) {
3453     # my $t1 = Time::HiRes::time;
3454     # for (1..1000) {
3455     # my $layout = CFPlus::Layout->new ($rgba);
3456     # $layout->set_text ("hallo" x 100);
3457     # $layout->render;
3458     # }
3459     # my $t2 = Time::HiRes::time;
3460     # warn $t2-$t1;
3461     # }
3462    
3463     $startup_done->();
3464    
3465     video_init;
3466     audio_init;
3467     }
3468    
3469     show_tip_of_the_day if $CFG->{show_tips};
3470    
3471     Event::loop;
3472     #CFPlus::SDL_Quit;
3473     #CFPlus::_exit 0;
3474    
3475     END {
3476     CFPlus::SDL_Quit;
3477     CFPlus::DB::Server::stop;
3478     }
3479    
3480     =head1 NAME
3481    
3482     cfplus - A Crossfire+ and Crossfire game client
3483    
3484     =head1 SYNOPSIS
3485    
3486     Just run it - no commandline arguments are supported.
3487    
3488     =head1 USAGE
3489    
3490     cfplus utilises OpenGL for all UI elements and the game. It is supposed to be used
3491     fullscreen and interactively.
3492    
3493     =head1 DEBUGGING
3494    
3495    
3496     CFPLUS_DEBUG - environment variable
3497    
3498     1 draw borders around widgets
3499     2 add low-level widget info to tooltips
3500     4 show fps
3501     8 suppress tooltips
3502    
3503     =head1 AUTHOR
3504    
3505     Marc Lehmann <crossfire@schmorp.de>, Robin Redeker <elmex@ta-sa.org>
3506    
3507    
3508    
3509 root 1.374 : $self->{vslider}->hide;
3510     }
3511 root 1.314 }
3512    
3513 root 1.239 sub update {
3514     my ($self) = @_;
3515    
3516     $self->SUPER::update;
3517 root 1.314 $self->update_slider;
3518 root 1.239 }
3519    
3520 root 1.375 sub invoke_mouse_wheel {
3521     my ($self, $ev) = @_;
3522    
3523     return 0 unless $ev->{dy}; # only vertical movements for now
3524    
3525     $self->{vslider}->emit (mouse_wheel => $ev);
3526    
3527     1
3528     }
3529    
3530     sub invoke_button_down {
3531     my ($self, $ev, $x, $y) = @_;
3532    
3533     if ($ev->{button} == 2) {
3534     $self->grab_focus;
3535 root 1.376
3536     my $ox = $self->{vp}{view_x} + $ev->{x};
3537     my $oy = $self->{vp}{view_y} + $ev->{y};
3538 root 1.375
3539     $self->{motion} = sub {
3540     my ($ev, $x, $y) = @_;
3541    
3542 root 1.376 $self->{vp}->set_offset ($ox - $ev->{x}, $oy - $ev->{y});
3543     $self->update;
3544 root 1.375 };
3545    
3546     return 1;
3547     }
3548    
3549     0
3550     }
3551    
3552     sub invoke_button_up {
3553     my ($self, $ev, $x, $y) = @_;
3554    
3555     if (delete $self->{motion}) {
3556     return 1;
3557     }
3558    
3559     0
3560     }
3561    
3562     sub invoke_mouse_motion {
3563     my ($self, $ev, $x, $y) = @_;
3564    
3565     if ($self->{motion}) {
3566     $self->{motion}->($ev, $x, $y);
3567     return 1;
3568     }
3569    
3570     0
3571     }
3572    
3573 root 1.305 sub invoke_size_allocate {
3574 root 1.259 my ($self, $w, $h) = @_;
3575 root 1.229
3576 root 1.314 $self->update_slider;
3577 root 1.305 $self->SUPER::invoke_size_allocate ($w, $h)
3578 root 1.229 }
3579    
3580 root 1.125 #############################################################################
3581    
3582 root 1.340 package CFPlus::UI::Frame;
3583 elmex 1.15
3584 root 1.340 our @ISA = CFPlus::UI::Bin::;
3585 elmex 1.15
3586 root 1.340 use CFPlus::OpenGL;
3587 elmex 1.15
3588 root 1.199 sub new {
3589     my $class = shift;
3590    
3591     $class->SUPER::new (
3592     bg => undef,
3593     @_,
3594     )
3595     }
3596    
3597     sub _draw {
3598     my ($self) = @_;
3599    
3600     if ($self->{bg}) {
3601     my ($w, $h) = @$self{qw(w h)};
3602    
3603     glEnable GL_BLEND;
3604 root 1.278 glBlendFunc GL_ONE, GL_ONE_MINUS_SRC_ALPHA;
3605     glColor_premultiply @{ $self->{bg} };
3606 root 1.199
3607     glBegin GL_QUADS;
3608     glVertex 0 , 0;
3609     glVertex 0 , $h;
3610     glVertex $w, $h;
3611     glVertex $w, 0;
3612     glEnd;
3613    
3614     glDisable GL_BLEND;
3615     }
3616    
3617     $self->SUPER::_draw;
3618     }
3619    
3620 root 1.39 #############################################################################
3621    
3622 root 1.340 package CFPlus::UI::FancyFrame;
3623 elmex 1.31
3624 root 1.340 our @ISA = CFPlus::UI::Bin::;
3625 elmex 1.31
3626 root 1.340 use CFPlus::OpenGL;
3627 elmex 1.31
3628 root 1.346 sub new {
3629     my ($class, %arg) = @_;
3630    
3631     if ((exists $arg{label}) && !ref $arg{label}) {
3632     $arg{label} = new CFPlus::UI::Label
3633     align => 1,
3634 root 1.347 valign => 0,
3635 root 1.346 text => $arg{label},
3636     fontsize => ($arg{border} || 0.8) * 0.75;
3637     }
3638    
3639     my $self = $class->SUPER::new (
3640     # label => "",
3641     fg => [0.6, 0.3, 0.1],
3642     border => 0.8,
3643     style => 'single',
3644     %arg,
3645     );
3646    
3647     $self
3648     }
3649    
3650     sub add {
3651     my ($self, @widgets) = @_;
3652    
3653     $self->SUPER::add (@widgets);
3654     $self->CFPlus::UI::Container::add ($self->{label}) if $self->{label};
3655     }
3656    
3657     sub border {
3658     int $_[0]{border} * $::FONTSIZE
3659     }
3660    
3661     sub size_request {
3662     my ($self) = @_;
3663    
3664     ($self->{label_w}, undef) = $self->{label}->size_request
3665     if $self->{label};
3666    
3667     my ($w, $h) = $self->SUPER::size_request;
3668    
3669     (
3670     $w + $self->border * 2,
3671     $h + $self->border * 2,
3672     )
3673     }
3674    
3675     sub invoke_size_allocate {
3676     my ($self, $w, $h) = @_;
3677    
3678     my $border = $self->border;
3679    
3680     $w -= List::Util::max 0, $border * 2;
3681     $h -= List::Util::max 0, $border * 2;
3682    
3683     if (my $label = $self->{label}) {
3684     $label->{w} = List::Util::max 0, List::Util::min $self->{label_w}, $w - $border * 2;
3685     $label->{h} = List::Util::min $h, $border;
3686     $label->invoke_size_allocate ($label->{w}, $label->{h});
3687     }
3688    
3689     $self->child->configure ($border, $border, $w, $h);
3690    
3691     1
3692     }
3693    
3694     sub _draw {
3695     my ($self) = @_;
3696    
3697     my $child = $self->{children}[0];
3698    
3699     my $border = $self->border;
3700     my ($w, $h) = ($self->{w}, $self->{h});
3701    
3702     $child->draw;
3703    
3704     glColor @{$self->{fg}};
3705     glBegin GL_LINE_STRIP;
3706     glVertex $border * 1.5 , $border * 0.5 + 0.5;
3707     glVertex $border * 0.5 + 0.5, $border * 0.5 + 0.5;
3708     glVertex $border * 0.5 + 0.5, $h - $border * 0.5 + 0.5;
3709     glVertex $w - $border * 0.5 + 0.5, $h - $border * 0.5 + 0.5;
3710     glVertex $w - $border * 0.5 + 0.5, $border * 0.5 + 0.5;
3711     glVertex $self->{label} ? $border * 2 + $self->{label}{w} : $border * 1.5, $border * 0.5 + 0.5;
3712     glEnd;
3713    
3714     if ($self->{label}) {
3715     glTranslate $border * 2, 0;
3716     $self->{label}->_draw;
3717     }
3718     }
3719    
3720     #############################################################################
3721    
3722     package CFPlus::UI::Toplevel;
3723    
3724     our @ISA = CFPlus::UI::Bin::;
3725    
3726     use CFPlus::OpenGL;
3727    
3728 root 1.255 my $bg =
3729 root 1.340 new_from_file CFPlus::Texture CFPlus::find_rcfile "d1_bg.png",
3730 root 1.255 mipmap => 1, wrap => 1;
3731    
3732     my @border =
3733 root 1.340 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
3734 root 1.255 qw(d1_border_top.png d1_border_right.png d1_border_left.png d1_border_bottom.png);
3735 elmex 1.34
3736 root 1.382 my @icon =
3737     map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
3738     qw(x1_move.png x1_resize.png);
3739    
3740 root 1.97 sub new {
3741 root 1.269 my ($class, %arg) = @_;
3742    
3743 root 1.141 my $self = $class->SUPER::new (
3744 root 1.230 bg => [1, 1, 1, 1],
3745     border_bg => [1, 1, 1, 1],
3746     border => 0.6,
3747     can_events => 1,
3748 root 1.326 min_w => 64,
3749     min_h => 32,
3750 root 1.269 %arg,
3751 root 1.141 );
3752    
3753 root 1.340 $self->{title_widget} = new CFPlus::UI::Label
3754 root 1.141 align => 0,
3755     valign => 1,
3756 root 1.302 text => $self->{title},
3757     fontsize => $self->{border},
3758     if exists $self->{title};
3759 root 1.141
3760 root 1.305 if ($self->{has_close_button}) {
3761     $self->{close_button} =
3762 root 1.340 new CFPlus::UI::ImageButton
3763 root 1.310 path => 'x1_close.png',
3764 root 1.317 on_activate => sub { $self->emit ("delete") };
3765 elmex 1.303
3766 root 1.340 $self->CFPlus::UI::Container::add ($self->{close_button});
3767 elmex 1.303 }
3768    
3769 root 1.141 $self
3770 root 1.97 }
3771    
3772 root 1.269 sub add {
3773     my ($self, @widgets) = @_;
3774    
3775     $self->SUPER::add (@widgets);
3776 root 1.340 $self->CFPlus::UI::Container::add ($self->{close_button}) if $self->{close_button};
3777     $self->CFPlus::UI::Container::add ($self->{title_widget}) if $self->{title_widget};
3778 root 1.269 }
3779    
3780 root 1.134 sub border {
3781     int $_[0]{border} * $::FONTSIZE
3782     }
3783    
3784 elmex 1.34 sub size_request {
3785     my ($self) = @_;
3786 root 1.39
3787 root 1.302 $self->{title_widget}->size_request
3788     if $self->{title_widget};
3789 root 1.269
3790 root 1.305 $self->{close_button}->size_request
3791     if $self->{close_button};
3792 elmex 1.303
3793 root 1.78 my ($w, $h) = $self->SUPER::size_request;
3794 elmex 1.34
3795 root 1.97 (
3796 root 1.134 $w + $self->border * 2,
3797     $h + $self->border * 2,
3798 root 1.97 )
3799 elmex 1.36 }
3800    
3801 root 1.305 sub invoke_size_allocate {
3802 root 1.259 my ($self, $w, $h) = @_;
3803 root 1.40
3804 root 1.302 if ($self->{title_widget}) {
3805     $self->{title_widget}{w} = $w;
3806     $self->{title_widget}{h} = $h;
3807 root 1.305 $self->{title_widget}->invoke_size_allocate ($w, $h);
3808 root 1.269 }
3809 elmex 1.36
3810 root 1.269 my $border = $self->border;
3811 root 1.141
3812 root 1.269 $h -= List::Util::max 0, $border * 2;
3813     $w -= List::Util::max 0, $border * 2;
3814 elmex 1.303
3815 root 1.269 $self->child->configure ($border, $border, $w, $h);
3816 elmex 1.303
3817 root 1.305 $self->{close_button}->configure ($self->{w} - $border, 0, $border, $border)
3818     if $self->{close_button};
3819    
3820     1
3821 elmex 1.34 }
3822    
3823 root 1.317 sub invoke_delete {
3824     my ($self) = @_;
3825    
3826     $self->hide;
3827    
3828     1
3829     }
3830    
3831 root 1.305 sub invoke_button_down {
3832 root 1.77 my ($self, $ev, $x, $y) = @_;
3833    
3834 root 1.176 my ($w, $h) = @$self{qw(w h)};
3835 root 1.134 my $border = $self->border;
3836    
3837 root 1.176 my $lr = ($x >= 0 && $x < $border) || ($x > $w - $border && $x < $w);
3838     my $td = ($y >= 0 && $y < $border) || ($y > $h - $border && $y < $h);
3839 root 1.77
3840 root 1.176 if ($lr & $td) {
3841     my ($wx, $wy) = ($self->{x}, $self->{y});
3842 root 1.139 my ($ox, $oy) = ($ev->{x}, $ev->{y});
3843 root 1.77 my ($bw, $bh) = ($self->{w}, $self->{h});
3844    
3845 root 1.176 my $mx = $x < $border;
3846     my $my = $y < $border;
3847    
3848 root 1.77 $self->{motion} = sub {
3849     my ($ev, $x, $y) = @_;
3850    
3851 root 1.176 my $dx = $ev->{x} - $ox;
3852     my $dy = $ev->{y} - $oy;
3853 root 1.77
3854 root 1.256 $self->{force_w} = $bw + $dx * ($mx ? -1 : 1);
3855     $self->{force_h} = $bh + $dy * ($my ? -1 : 1);
3856    
3857 root 1.277 $self->move_abs ($wx + $dx * $mx, $wy + $dy * $my);
3858 root 1.251 $self->realloc;
3859 root 1.77 };
3860    
3861 root 1.176 } elsif ($lr ^ $td) {
3862 root 1.139 my ($ox, $oy) = ($ev->{x}, $ev->{y});
3863 root 1.77 my ($bx, $by) = ($self->{x}, $self->{y});
3864    
3865     $self->{motion} = sub {
3866     my ($ev, $x, $y) = @_;
3867    
3868 root 1.139 ($x, $y) = ($ev->{x}, $ev->{y});
3869 root 1.77
3870 root 1.256 $self->move_abs ($bx + $x - $ox, $by + $y - $oy);
3871 root 1.277 # HACK: the next line is required to enforce placement
3872 root 1.305 $self->{parent}->invoke_size_allocate ($self->{parent}{w}, $self->{parent}{h});
3873 root 1.77 };
3874 root 1.271 } else {
3875     return 0;
3876 root 1.77 }
3877 root 1.271
3878     1
3879 root 1.77 }
3880    
3881 root 1.305 sub invoke_button_up {
3882 root 1.77 my ($self, $ev, $x, $y) = @_;
3883    
3884 root 1.305 ! ! delete $self->{motion}
3885 root 1.77 }
3886    
3887 root 1.305 sub invoke_mouse_motion {
3888 root 1.77 my ($self, $ev, $x, $y) = @_;
3889    
3890     $self->{motion}->($ev, $x, $y) if $self->{motion};
3891 root 1.271
3892 root 1.305 ! ! $self->{motion}
3893 root 1.77 }
3894    
3895 root 1.339 sub invoke_visibility_change {
3896     my ($self, $visible) = @_;
3897    
3898     delete $self->{motion} unless $visible;
3899    
3900     0
3901     }
3902    
3903 elmex 1.34 sub _draw {
3904     my ($self) = @_;
3905    
3906 root 1.269 my $child = $self->{children}[0];
3907    
3908 root 1.97 my ($w, $h ) = ($self->{w}, $self->{h});
3909 root 1.269 my ($cw, $ch) = ($child->{w}, $child->{h});
3910 elmex 1.34
3911     glEnable GL_TEXTURE_2D;
3912 root 1.97 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE;
3913 elmex 1.34
3914 root 1.134 my $border = $self->border;
3915    
3916 root 1.97 glColor @{ $self->{border_bg} };
3917 root 1.382 $border[0]->draw_quad_alpha ( 0, 0, $w, $border);
3918     $border[1]->draw_quad_alpha ( 0, $border, $border, $ch);
3919     $border[2]->draw_quad_alpha ($w - $border, $border, $border, $ch);
3920     $border[3]->draw_quad_alpha ( 0, $h - $border, $w, $border);
3921    
3922     # move
3923     my $w2 = ($w - $border) * .5;
3924     my $h2 = ($h - $border) * .5;
3925     $icon[0]->draw_quad_alpha ( 0, $h2, $border, $border);
3926     $icon[0]->draw_quad_alpha ($w - $border, $h2, $border, $border);
3927     $icon[0]->draw_quad_alpha ($w2 , $h - $border, $border, $border);
3928    
3929     # resize
3930     $icon[1]->draw_quad_alpha ( 0, 0, $border, $border);
3931     $icon[1]->draw_quad_alpha ($w - $border, 0, $border, $border)
3932     unless $self->{has_close_button};
3933     $icon[1]->draw_quad_alpha ( 0, $h - $border, $border, $border);
3934     $icon[1]->draw_quad_alpha ($w - $border, $h - $border, $border, $border);
3935 elmex 1.34
3936 root 1.177 if (@{$self->{bg}} < 4 || $self->{bg}[3]) {
3937 root 1.255 glColor @{ $self->{bg} };
3938 root 1.76
3939 root 1.177 # TODO: repeat texture not scale
3940 root 1.255 # solve this better(?)
3941     $bg->{s} = $cw / $bg->{w};
3942     $bg->{t} = $ch / $bg->{h};
3943 root 1.197 $bg->draw_quad_alpha ($border, $border, $cw, $ch);
3944     }
3945 elmex 1.34
3946 root 1.197 glDisable GL_TEXTURE_2D;
3947 elmex 1.36
3948 root 1.269 $child->draw;
3949 root 1.177
3950 root 1.302 if ($self->{title_widget}) {
3951 root 1.269 glTranslate 0, $border - $self->{h};
3952 root 1.302 $self->{title_widget}->_draw;
3953 elmex 1.303
3954     glTranslate 0, - ($border - $self->{h});
3955 root 1.269 }
3956 elmex 1.303
3957 root 1.305 $self->{close_button}->draw
3958     if $self->{close_button};
3959 elmex 1.34 }
3960 elmex 1.31
3961 root 1.39 #############################################################################
3962    
3963 root 1.340 package CFPlus::UI::Table;
3964 elmex 1.15
3965 root 1.340 our @ISA = CFPlus::UI::Base::;
3966 elmex 1.15
3967 root 1.75 use List::Util qw(max sum);
3968    
3969 root 1.340 use CFPlus::OpenGL;
3970 elmex 1.15
3971 root 1.78 sub new {
3972     my $class = shift;
3973    
3974     $class->SUPER::new (
3975 root 1.374 children => [],
3976 root 1.78 col_expand => [],
3977 root 1.369 row_expand => [],
3978 root 1.234 @_,
3979 root 1.78 )
3980     }
3981    
3982 root 1.236 sub children {
3983     grep $_, map @$_, grep $_, @{ $_[0]{children} }
3984     }
3985    
3986 root 1.374 # TODO: store row/col info in child widget and use standard add/del
3987 elmex 1.15 sub add {
3988 root 1.383 my $self = shift;
3989    
3990     Carp::cluck "please use the add_at method instead of calling add, thank you.\n";#d#
3991     $self->add_at (@_);
3992     }
3993    
3994     sub add_at {
3995     my $self = shift;
3996 elmex 1.32
3997 root 1.332 while (@_) {
3998 root 1.383 my ($col, $row, $child) = splice @_, 0, 3, ();
3999    
4000 root 1.332 $child->set_parent ($self);
4001 root 1.383 $self->{children}[$row][$col] = $child;
4002 root 1.332 }
4003 root 1.75
4004 root 1.374 $self->{force_realloc} = 1;
4005 root 1.332 $self->{force_size_alloc} = 1;
4006 root 1.251 $self->realloc;
4007 root 1.172 }
4008    
4009 root 1.302 sub remove {
4010     my ($self, $child) = @_;
4011    
4012 root 1.374 for (@{ $self->{children} }) {
4013     for (@{ $_ || [] }) {
4014     $_ = undef if $_ == $child;
4015     }
4016     }
4017 root 1.302 }
4018    
4019 root 1.236 # TODO: move to container class maybe? send children a signal on removal?
4020 root 1.115 sub clear {
4021     my ($self) = @_;
4022    
4023 root 1.172 my @children = $self->children;
4024     delete $self->{children};
4025 root 1.163
4026 root 1.172 for (@children) {
4027 root 1.163 delete $_->{parent};
4028     $_->hide;
4029     }
4030    
4031 root 1.251 $self->realloc;
4032 root 1.115 }
4033    
4034 root 1.75 sub get_wh {
4035     my ($self) = @_;
4036    
4037     my (@w, @h);
4038 elmex 1.15
4039 root 1.75 for my $y (0 .. $#{$self->{children}}) {
4040     my $row = $self->{children}[$y]
4041     or next;
4042 elmex 1.15
4043 root 1.75 for my $x (0 .. $#$row) {
4044     my $widget = $row->[$x]
4045     or next;
4046 root 1.149 my ($w, $h) = @$widget{qw(req_w req_h)};
4047 elmex 1.15
4048 root 1.75 $w[$x] = max $w[$x], $w;
4049     $h[$y] = max $h[$y], $h;
4050 elmex 1.17 }
4051 elmex 1.15 }
4052 root 1.75
4053     (\@w, \@h)
4054 elmex 1.15 }
4055    
4056     sub size_request {
4057     my ($self) = @_;
4058    
4059 root 1.75 my ($ws, $hs) = $self->get_wh;
4060 elmex 1.15
4061 root 1.75 (
4062 root 1.78 (sum @$ws),
4063     (sum @$hs),
4064 root 1.75 )
4065     }
4066    
4067 root 1.305 sub invoke_size_allocate {
4068 root 1.259 my ($self, $w, $h) = @_;
4069 root 1.75
4070     my ($ws, $hs) = $self->get_wh;
4071    
4072 root 1.238 my $req_w = (sum @$ws) || 1;
4073     my $req_h = (sum @$hs) || 1;
4074 root 1.78
4075 root 1.369 # TODO: nicer code
4076 root 1.78 my @col_expand = @{$self->{col_expand}};
4077     @col_expand = (1) x @$ws unless @col_expand;
4078     my $col_expand = (sum @col_expand) || 1;
4079 elmex 1.15
4080 root 1.78 $ws->[$_] += $col_expand[$_] / $col_expand * ($w - $req_w) for 0 .. $#$ws;
4081 elmex 1.15
4082 root 1.340 CFPlus::UI::harmonize $ws;
4083 root 1.369
4084     my @row_expand = @{$self->{row_expand}};
4085     @row_expand = (1) x @$ws unless @row_expand;
4086     my $row_expand = (sum @row_expand) || 1;
4087    
4088     $hs->[$_] += $row_expand[$_] / $row_expand * ($h - $req_h) for 0 .. $#$hs;
4089    
4090 root 1.340 CFPlus::UI::harmonize $hs;
4091 root 1.106
4092 root 1.75 my $y;
4093 elmex 1.15
4094 root 1.75 for my $r (0 .. $#{$self->{children}}) {
4095     my $row = $self->{children}[$r]
4096     or next;
4097 elmex 1.15
4098     my $x = 0;
4099 root 1.75 my $row_h = $hs->[$r];
4100    
4101     for my $c (0 .. $#$row) {
4102     my $col_w = $ws->[$c];
4103 elmex 1.15
4104 root 1.83 if (my $widget = $row->[$c]) {
4105 root 1.128 $widget->configure ($x, $y, $col_w, $row_h);
4106 root 1.83 }
4107 elmex 1.15
4108 root 1.75 $x += $col_w;
4109 elmex 1.15 }
4110    
4111 root 1.75 $y += $row_h;
4112     }
4113    
4114 root 1.305 1
4115 root 1.75 }
4116    
4117 root 1.76 sub find_widget {
4118     my ($self, $x, $y) = @_;
4119    
4120     $x -= $self->{x};
4121     $y -= $self->{y};
4122    
4123     my $res;
4124    
4125     for (grep $_, map @$_, grep $_, @{ $self->{children} }) {
4126     $res = $_->find_widget ($x, $y)
4127     and return $res;
4128     }
4129    
4130     $self->SUPER::find_widget ($x + $self->{x}, $y + $self->{y})
4131     }
4132    
4133 root 1.75 sub _draw {
4134     my ($self) = @_;
4135    
4136     for (grep $_, @{$self->{children}}) {
4137     $_->draw for grep $_, @$_;
4138 elmex 1.15 }
4139     }
4140    
4141 root 1.39 #############################################################################
4142    
4143 root 1.374 package CFPlus::UI::Fixed;
4144    
4145     use List::Util qw(min max);
4146    
4147     our @ISA = CFPlus::UI::Container::;
4148    
4149     sub add {
4150     my ($self, $child, $posmode, $x, $y, $sizemode, $w, $h) = @_;
4151    
4152     $child->{_fixed} = [$posmode, $x, $y, $sizemode, $w, $h];
4153     $self->SUPER::add ($child);
4154     }
4155    
4156     sub _scale($$$) {
4157     my ($mode, $val, $max) = @_;
4158    
4159     $mode eq "abs" ? $val
4160     : $mode eq "rel" ? $val * $max
4161     : 0
4162     }
4163    
4164     sub size_request {
4165     my ($self) = @_;
4166    
4167     my ($x1, $y1, $x2, $y2) = (0, 0, 0, 0);
4168    
4169     # determine overall size by querying abs widgets
4170     for my $child ($self->visible_children) {
4171     my ($pos, $x, $y, $size, $w, $h) = @{ $child->{_fixed} };
4172    
4173     if ($pos eq "abs") {
4174     $w = _scale $size, $w, $child->{req_w};
4175     $h = _scale $size, $h, $child->{req_h};
4176    
4177     $x1 = min $x1, $x; $x2 = max $x2, $x + $w;
4178     $y1 = min $y1, $y; $y2 = max $y2, $y + $h;
4179     }
4180     }
4181    
4182     my $W = $x2 - $x1;
4183     my $H = $y2 - $y1;
4184    
4185     # now layout remaining widgets
4186     for my $child ($self->visible_children) {
4187     my ($pos, $x, $y, $size, $w, $h) = @{ $child->{_fixed} };
4188    
4189     if ($pos ne "abs") {
4190     $x = _scale $pos, $x, $W;
4191     $y = _scale $pos, $x, $H;
4192     $w = _scale $size, $w, $child->{req_w};
4193     $h = _scale $size, $h, $child->{req_h};
4194    
4195     $x1 = min $x1, $x; $x2 = max $x2, $x + $w;
4196     $y1 = min $y1, $y; $y2 = max $y2, $y + $h;
4197     }
4198     }
4199    
4200     my $W = $x2 - $x1;
4201     my $H = $y2 - $y1;
4202    
4203     ($W, $H)
4204     }
4205    
4206     sub invoke_size_allocate {
4207     my ($self, $W, $H) = @_;
4208    
4209     for my $child ($self->visible_children) {
4210     my ($pos, $x, $y, $size, $w, $h) = @{ $child->{_fixed} };
4211    
4212     $x = _scale $pos, $x, $W;
4213     $y = _scale $pos, $x, $H;
4214     $w = _scale $size, $w, $child->{req_w};
4215     $h = _scale $size, $h, $child->{req_h};
4216    
4217     $child->configure ($x, $y, $w, $h);
4218     }
4219    
4220     1
4221     }
4222    
4223     #############################################################################
4224    
4225 root 1.340 package CFPlus::UI::Box;
4226 root 1.76
4227 root 1.340 our @ISA = CFPlus::UI::Container::;
4228 root 1.76
4229     sub size_request {
4230     my ($self) = @_;
4231    
4232 root 1.246 $self->{vertical}
4233     ? (
4234     (List::Util::max map $_->{req_w}, @{$self->{children}}),
4235     (List::Util::sum map $_->{req_h}, @{$self->{children}}),
4236     )
4237     : (
4238     (List::Util::sum map $_->{req_w}, @{$self->{children}}),
4239     (List::Util::max map $_->{req_h}, @{$self->{children}}),
4240     )
4241 root 1.76 }
4242    
4243 root 1.305 sub invoke_size_allocate {
4244 root 1.259 my ($self, $w, $h) = @_;
4245 root 1.76
4246 root 1.246 my $space = $self->{vertical} ? $h : $w;
4247 root 1.310 my @children = $self->visible_children;
4248 root 1.76
4249 root 1.247 my @req;
4250 root 1.76
4251 root 1.247 if ($self->{homogeneous}) {
4252 root 1.310 @req = ($space / (@children || 1)) x @children;
4253 root 1.76 } else {
4254 root 1.310 @req = map $_->{$self->{vertical} ? "req_h" : "req_w"}, @children;
4255 root 1.247 my $req = List::Util::sum @req;
4256    
4257     if ($req > $space) {
4258     # ah well, not enough space
4259     $_ *= $space / $req for @req;
4260     } else {
4261 root 1.310 my $expand = (List::Util::sum map $_->{expand}, @children) || 1;
4262 root 1.247
4263     $space = ($space - $req) / $expand; # remaining space to give away
4264    
4265 root 1.310 $req[$_] += $space * $children[$_]{expand}
4266     for 0 .. $#children;
4267 root 1.247 }
4268 root 1.76 }
4269    
4270 root 1.340 CFPlus::UI::harmonize \@req;
4271 root 1.112
4272 root 1.246 my $pos = 0;
4273 root 1.310 for (0 .. $#children) {
4274 root 1.246 my $alloc = $req[$_];
4275 root 1.310 $children[$_]->configure ($self->{vertical} ? (0, $pos, $w, $alloc) : ($pos, 0, $alloc, $h));
4276 root 1.76
4277 root 1.246 $pos += $alloc;
4278 root 1.76 }
4279 root 1.125
4280     1
4281 root 1.76 }
4282    
4283     #############################################################################
4284    
4285 root 1.340 package CFPlus::UI::HBox;
4286 elmex 1.15
4287 root 1.340 our @ISA = CFPlus::UI::Box::;
4288 root 1.76
4289 root 1.246 sub new {
4290     my $class = shift;
4291 elmex 1.15
4292 root 1.246 $class->SUPER::new (
4293     vertical => 0,
4294     @_,
4295 root 1.43 )
4296     }
4297    
4298 root 1.246 #############################################################################
4299 root 1.68
4300 root 1.340 package CFPlus::UI::VBox;
4301 root 1.193
4302 root 1.340 our @ISA = CFPlus::UI::Box::;
4303 root 1.68
4304 root 1.246 sub new {
4305     my $class = shift;
4306 root 1.68
4307 root 1.246 $class->SUPER::new (
4308     vertical => 1,
4309     @_,
4310     )
4311 elmex 1.36 }
4312    
4313 root 1.39 #############################################################################
4314    
4315 root 1.340 package CFPlus::UI::Label;
4316 root 1.10
4317 root 1.340 our @ISA = CFPlus::UI::DrawBG::;
4318 root 1.12
4319 root 1.340 use CFPlus::OpenGL;
4320 root 1.10
4321     sub new {
4322 root 1.64 my ($class, %arg) = @_;
4323 root 1.51
4324 root 1.59 my $self = $class->SUPER::new (
4325 root 1.164 fg => [1, 1, 1],
4326 root 1.209 #bg => none
4327     #active_bg => none
4328 root 1.164 #font => default_font
4329 root 1.194 #text => initial text
4330     #markup => initial narkup
4331 root 1.213 #max_w => maximum pixel width
4332 root 1.343 #style => 0, # render flags
4333 root 1.213 ellipsise => 3, # end
4334 root 1.340 layout => (new CFPlus::Layout),
4335 root 1.164 fontsize => 1,
4336     align => -1,
4337     valign => -1,
4338 root 1.258 padding_x => 2,
4339     padding_y => 2,
4340 elmex 1.150 can_events => 0,
4341 root 1.64 %arg
4342 root 1.59 );
4343 root 1.10
4344 root 1.141 if (exists $self->{template}) {
4345 root 1.340 my $layout = new CFPlus::Layout;
4346 root 1.141 $layout->set_text (delete $self->{template});
4347     $self->{template} = $layout;
4348     }
4349 root 1.121
4350 root 1.194 if (exists $self->{markup}) {
4351     $self->set_markup (delete $self->{markup});
4352     } else {
4353     $self->set_text (delete $self->{text});
4354     }
4355 root 1.10
4356     $self
4357     }
4358    
4359 root 1.173 sub update {
4360     my ($self) = @_;
4361    
4362     delete $self->{texture};
4363     $self->SUPER::update;
4364     }
4365    
4366 root 1.282 sub realloc {
4367     my ($self) = @_;
4368    
4369     delete $self->{ox};
4370     $self->SUPER::realloc;
4371     }
4372    
4373 elmex 1.15 sub set_text {
4374     my ($self, $text) = @_;
4375 root 1.28
4376 root 1.173 return if $self->{text} eq "T$text";
4377     $self->{text} = "T$text";
4378    
4379 root 1.121 $self->{layout}->set_text ($text);
4380 root 1.288
4381 root 1.289 delete $self->{size_req};
4382 root 1.251 $self->realloc;
4383 root 1.252 $self->update;
4384 elmex 1.15 }
4385    
4386 root 1.121 sub set_markup {
4387     my ($self, $markup) = @_;
4388    
4389 root 1.173 return if $self->{text} eq "M$markup";
4390     $self->{text} = "M$markup";
4391    
4392 root 1.194 my $rgba = $markup =~ /span.*(?:foreground|background)/;
4393    
4394 root 1.121 $self->{layout}->set_markup ($markup);
4395 root 1.288
4396 root 1.289 delete $self->{size_req};
4397 root 1.251 $self->realloc;
4398 root 1.252 $self->update;
4399 elmex 1.15 }
4400    
4401 root 1.14 sub size_request {
4402     my ($self) = @_;
4403    
4404 root 1.289 $self->{size_req} ||= do {
4405 root 1.282 $self->{layout}->set_font ($self->{font}) if $self->{font};
4406     $self->{layout}->set_width ($self->{max_w} || -1);
4407     $self->{layout}->set_ellipsise ($self->{ellipsise});
4408     $self->{layout}->set_single_paragraph_mode ($self->{ellipsise});
4409     $self->{layout}->set_height ($self->{fontsize} * $::FONTSIZE);
4410    
4411     my ($w, $h) = $self->{layout}->size;
4412 root 1.121
4413 root 1.282 if (exists $self->{template}) {
4414     $self->{template}->set_font ($self->{font}) if $self->{font};
4415 root 1.323 $self->{template}->set_width ($self->{max_w} || -1);
4416 root 1.282 $self->{template}->set_height ($self->{fontsize} * $::FONTSIZE);
4417 root 1.76
4418 root 1.282 my ($w2, $h2) = $self->{template}->size;
4419 root 1.141
4420 root 1.282 $w = List::Util::max $w, $w2;
4421     $h = List::Util::max $h, $h2;
4422     }
4423 root 1.141
4424 root 1.289 [$w, $h]
4425     };
4426    
4427     @{ $self->{size_req} }
4428 root 1.59 }
4429 root 1.51
4430 root 1.311 sub baseline_shift {
4431     $_[0]{layout}->descent
4432     }
4433    
4434 root 1.305 sub invoke_size_allocate {
4435 root 1.259 my ($self, $w, $h) = @_;
4436 root 1.68
4437 root 1.269 delete $self->{ox};
4438    
4439 root 1.264 delete $self->{texture}
4440 root 1.266 unless $w >= $self->{req_w} && $self->{old_w} >= $self->{req_w};
4441 root 1.305
4442     1
4443 root 1.14 }
4444    
4445 elmex 1.146 sub set_fontsize {
4446     my ($self, $fontsize) = @_;
4447    
4448     $self->{fontsize} = $fontsize;
4449 root 1.316 delete $self->{size_req};
4450 root 1.152 delete $self->{texture};
4451 root 1.186
4452 root 1.251 $self->realloc;
4453 elmex 1.146 }
4454    
4455 root 1.289 sub reconfigure {
4456     my ($self) = @_;
4457    
4458     delete $self->{size_req};
4459 root 1.320 delete $self->{texture};
4460 root 1.289
4461     $self->SUPER::reconfigure;
4462     }
4463    
4464 elmex 1.11 sub _draw {
4465 root 1.10 my ($self) = @_;
4466    
4467 root 1.209 $self->SUPER::_draw; # draw background, if applicable
4468    
4469 root 1.320 my $size = $self->{texture} ||= do {
4470 root 1.194 $self->{layout}->set_foreground (@{$self->{fg}});
4471 root 1.157 $self->{layout}->set_font ($self->{font}) if $self->{font};
4472 root 1.59 $self->{layout}->set_width ($self->{w});
4473 root 1.213 $self->{layout}->set_ellipsise ($self->{ellipsise});
4474     $self->{layout}->set_single_paragraph_mode ($self->{ellipsise});
4475     $self->{layout}->set_height ($self->{fontsize} * $::FONTSIZE);
4476 root 1.194
4477 root 1.323 [$self->{layout}->size]
4478 root 1.269 };
4479 root 1.194
4480 root 1.269 unless (exists $self->{ox}) {
4481 root 1.258 $self->{ox} = int ($self->{align} < 0 ? $self->{padding_x}
4482 root 1.320 : $self->{align} > 0 ? $self->{w} - $size->[0] - $self->{padding_x}
4483     : ($self->{w} - $size->[0]) * 0.5);
4484 root 1.208
4485 root 1.258 $self->{oy} = int ($self->{valign} < 0 ? $self->{padding_y}
4486 root 1.320 : $self->{valign} > 0 ? $self->{h} - $size->[1] - $self->{padding_y}
4487     : ($self->{h} - $size->[1]) * 0.5);
4488 root 1.59 };
4489 root 1.10
4490 root 1.320 my $w = List::Util::min $self->{w} + 4, $size->[0];
4491     my $h = List::Util::min $self->{h} + 2, $size->[1];
4492 root 1.294
4493 root 1.343 $self->{layout}->render ($self->{ox}, $self->{oy}, $self->{style});
4494 root 1.10 }
4495    
4496 root 1.39 #############################################################################
4497    
4498 root 1.340 package CFPlus::UI::EntryBase;
4499 elmex 1.31
4500 root 1.340 our @ISA = CFPlus::UI::Label::;
4501 elmex 1.31
4502 root 1.340 use CFPlus::OpenGL;
4503 elmex 1.31
4504 root 1.68 sub new {
4505     my $class = shift;
4506    
4507     $class->SUPER::new (
4508 root 1.164 fg => [1, 1, 1],
4509     bg => [0, 0, 0, 0.2],
4510     active_bg => [1, 1, 1, 0.5],
4511     active_fg => [0, 0, 0],
4512     can_hover => 1,
4513     can_focus => 1,
4514     valign => 0,
4515 elmex 1.150 can_events => 1,
4516 root 1.358 ellipsise => 0,
4517 root 1.225 #text => ...
4518 root 1.291 #hidden => "*",
4519 root 1.68 @_
4520     )
4521     }
4522    
4523     sub _set_text {
4524     my ($self, $text) = @_;
4525    
4526 root 1.121 delete $self->{cur_h};
4527    
4528     return if $self->{text} eq $text;
4529 elmex 1.100
4530 root 1.68 $self->{last_activity} = $::NOW;
4531     $self->{text} = $text;
4532 root 1.72
4533     $text =~ s/./*/g if $self->{hidden};
4534 root 1.121 $self->{layout}->set_text ("$text ");
4535 root 1.289 delete $self->{size_req};
4536 root 1.72
4537 root 1.305 $self->emit (changed => $self->{text});
4538 root 1.283
4539     $self->realloc;
4540 root 1.276 $self->update;
4541 root 1.121 }
4542 root 1.68
4543 root 1.194 sub set_text {
4544     my ($self, $text) = @_;
4545    
4546     $self->{cursor} = length $text;
4547     $self->_set_text ($text);
4548     }
4549    
4550 root 1.121 sub get_text {
4551     $_[0]{text}
4552 root 1.68 }
4553    
4554     sub size_request {
4555     my ($self) = @_;
4556    
4557     my ($w, $h) = $self->SUPER::size_request;
4558    
4559     ($w + 1, $h) # add 1 for cursor
4560     }
4561    
4562 root 1.305 sub invoke_key_down {
4563 elmex 1.31 my ($self, $ev) = @_;
4564    
4565 root 1.137 my $mod = $ev->{mod};
4566     my $sym = $ev->{sym};
4567     my $uni = $ev->{unicode};
4568 elmex 1.31
4569     my $text = $self->get_text;
4570    
4571 root 1.367 $self->{cursor} = List::Util::max 0, List::Util::min $self->{cursor}, length $text;
4572    
4573 root 1.200 if ($uni == 8) {
4574 root 1.68 substr $text, --$self->{cursor}, 1, "" if $self->{cursor};
4575 root 1.200 } elsif ($uni == 127) {
4576 root 1.68 substr $text, $self->{cursor}, 1, "";
4577 root 1.340 } elsif ($sym == CFPlus::SDLK_LEFT) {
4578 root 1.68 --$self->{cursor} if $self->{cursor};
4579 root 1.340 } elsif ($sym == CFPlus::SDLK_RIGHT) {
4580 root 1.68 ++$self->{cursor} if $self->{cursor} < length $self->{text};
4581 root 1.340 } elsif ($sym == CFPlus::SDLK_HOME) {
4582 root 1.363 # what a hack
4583     $self->{cursor} =
4584     (substr $self->{text}, 0, $self->{cursor}) =~ /^(.*\012)/
4585     ? length $1
4586     : 0;
4587 root 1.340 } elsif ($sym == CFPlus::SDLK_END) {
4588 root 1.363 # uh, again
4589     $self->{cursor} =
4590     (substr $self->{text}, $self->{cursor}) =~ /^([^\012]*)\012/
4591     ? $self->{cursor} + length $1
4592     : length $self->{text};
4593 root 1.360 } elsif ($uni == 21) { # ctrl-u
4594 root 1.348 $text = "";
4595 root 1.349 $self->{cursor} = 0;
4596 root 1.200 } elsif ($uni == 27) {
4597 root 1.305 $self->emit ('escape');
4598 root 1.363 } elsif ($uni == 0x0d) {
4599     substr $text, $self->{cursor}++, 0, "\012";
4600     } elsif ($uni >= 0x20) {
4601 root 1.68 substr $text, $self->{cursor}++, 0, chr $uni;
4602 root 1.271 } else {
4603     return 0;
4604 elmex 1.31 }
4605 root 1.51
4606 root 1.68 $self->_set_text ($text);
4607 root 1.251
4608     $self->realloc;
4609 root 1.358 $self->update;
4610 root 1.271
4611     1
4612 root 1.68 }
4613    
4614 root 1.305 sub invoke_focus_in {
4615 root 1.68 my ($self) = @_;
4616    
4617     $self->{last_activity} = $::NOW;
4618    
4619 root 1.305 $self->SUPER::invoke_focus_in
4620 elmex 1.31 }
4621    
4622 root 1.305 sub invoke_button_down {
4623 root 1.68 my ($self, $ev, $x, $y) = @_;
4624    
4625 root 1.305 $self->SUPER::invoke_button_down ($ev, $x, $y);
4626 root 1.68
4627     my $idx = $self->{layout}->xy_to_index ($x, $y);
4628    
4629     # byte-index to char-index
4630 root 1.76 my $text = $self->{text};
4631 root 1.325 utf8::encode $text; $text = substr $text, 0, $idx; utf8::decode $text;
4632     $self->{cursor} = length $text;
4633 root 1.51
4634 root 1.68 $self->_set_text ($self->{text});
4635     $self->update;
4636 root 1.271
4637     1
4638 root 1.51 }
4639    
4640 root 1.305 sub invoke_mouse_motion {
4641 root 1.58 my ($self, $ev, $x, $y) = @_;
4642 root 1.68 # printf "M %d,%d %d,%d\n", $ev->motion_x, $ev->motion_y, $x, $y;#d#
4643 root 1.271
4644 root 1.305 1
4645 root 1.58 }
4646    
4647 root 1.51 sub _draw {
4648     my ($self) = @_;
4649    
4650 root 1.68 local $self->{fg} = $self->{fg};
4651    
4652 root 1.51 if ($FOCUS == $self) {
4653 root 1.278 glColor_premultiply @{$self->{active_bg}};
4654 root 1.68 $self->{fg} = $self->{active_fg};
4655 root 1.51 } else {
4656 root 1.278 glColor_premultiply @{$self->{bg}};
4657 root 1.51 }
4658    
4659 root 1.76 glEnable GL_BLEND;
4660 root 1.278 glBlendFunc GL_ONE, GL_ONE_MINUS_SRC_ALPHA;
4661 root 1.51 glBegin GL_QUADS;
4662 root 1.68 glVertex 0 , 0;
4663     glVertex 0 , $self->{h};
4664     glVertex $self->{w}, $self->{h};
4665     glVertex $self->{w}, 0;
4666 root 1.51 glEnd;
4667 root 1.76 glDisable GL_BLEND;
4668 root 1.51
4669     $self->SUPER::_draw;
4670 root 1.68
4671     #TODO: force update every cursor change :(
4672     if ($FOCUS == $self && (($::NOW - $self->{last_activity}) & 1023) < 600) {
4673 root 1.121
4674     unless (exists $self->{cur_h}) {
4675     my $text = substr $self->{text}, 0, $self->{cursor};
4676     utf8::encode $text;
4677    
4678     @$self{qw(cur_x cur_y cur_h)} = $self->{layout}->cursor_pos (length $text)
4679     }
4680    
4681 root 1.68 glBegin GL_LINES;
4682 root 1.344 glVertex 0.5 + $self->{cur_x} + $self->{ox}, $self->{cur_y} + $self->{oy};
4683     glVertex 0.5 + $self->{cur_x} + $self->{ox}, $self->{cur_y} + $self->{oy} + $self->{cur_h};
4684 root 1.68 glEnd;
4685     }
4686     }
4687    
4688 root 1.358 #############################################################################
4689    
4690 root 1.340 package CFPlus::UI::Entry;
4691 elmex 1.99
4692 root 1.340 our @ISA = CFPlus::UI::EntryBase::;
4693 elmex 1.99
4694 root 1.340 use CFPlus::OpenGL;
4695 elmex 1.99
4696 root 1.305 sub invoke_key_down {
4697 elmex 1.99 my ($self, $ev) = @_;
4698    
4699 root 1.137 my $sym = $ev->{sym};
4700 elmex 1.99
4701 root 1.360 if ($ev->{uni} == 0x0d || $sym == 13) {
4702 elmex 1.167 unshift @{$self->{history}},
4703     my $txt = $self->get_text;
4704 root 1.306
4705 elmex 1.167 $self->{history_pointer} = -1;
4706 elmex 1.169 $self->{history_saveback} = '';
4707 root 1.305 $self->emit (activate => $txt);
4708 elmex 1.99 $self->update;
4709    
4710 root 1.340 } elsif ($sym == CFPlus::SDLK_UP) {
4711 elmex 1.167 if ($self->{history_pointer} < 0) {
4712     $self->{history_saveback} = $self->get_text;
4713     }
4714 elmex 1.169 if (@{$self->{history} || []} > 0) {
4715     $self->{history_pointer}++;
4716     if ($self->{history_pointer} >= @{$self->{history} || []}) {
4717     $self->{history_pointer} = @{$self->{history} || []} - 1;
4718     }
4719     $self->set_text ($self->{history}->[$self->{history_pointer}]);
4720 elmex 1.167 }
4721    
4722 root 1.340 } elsif ($sym == CFPlus::SDLK_DOWN) {
4723 elmex 1.167 $self->{history_pointer}--;
4724     $self->{history_pointer} = -1 if $self->{history_pointer} < 0;
4725    
4726     if ($self->{history_pointer} >= 0) {
4727     $self->set_text ($self->{history}->[$self->{history_pointer}]);
4728     } else {
4729     $self->set_text ($self->{history_saveback});
4730     }
4731    
4732 root 1.360 } else {
4733 root 1.305 return $self->SUPER::invoke_key_down ($ev)
4734 elmex 1.99 }
4735    
4736 root 1.271 1
4737 elmex 1.99 }
4738    
4739 root 1.68 #############################################################################
4740    
4741 root 1.358 package CFPlus::UI::TextEdit;
4742    
4743     our @ISA = CFPlus::UI::EntryBase::;
4744    
4745     use CFPlus::OpenGL;
4746    
4747     sub move_cursor_ver {
4748     my ($self, $dy) = @_;
4749    
4750     my ($y, $x) = $self->{layout}->index_to_line_x ($self->{cursor});
4751    
4752 root 1.359 $y += $dy;
4753    
4754     if (defined (my $index = $self->{layout}->line_x_to_index ($y, $x))) {
4755     $self->{cursor} = $index;
4756     delete $self->{cur_h};
4757     $self->update;
4758     return;
4759     }
4760 root 1.358 }
4761    
4762     sub invoke_key_down {
4763     my ($self, $ev) = @_;
4764    
4765     my $sym = $ev->{sym};
4766    
4767     if ($sym == CFPlus::SDLK_UP) {
4768     $self->move_cursor_ver (-1);
4769     } elsif ($sym == CFPlus::SDLK_DOWN) {
4770     $self->move_cursor_ver (+1);
4771     } else {
4772     return $self->SUPER::invoke_key_down ($ev)
4773     }
4774    
4775     1
4776     }
4777    
4778     #############################################################################
4779    
4780 root 1.340 package CFPlus::UI::Button;
4781 root 1.79
4782 root 1.340 our @ISA = CFPlus::UI::Label::;
4783 root 1.79
4784 root 1.340 use CFPlus::OpenGL;
4785 root 1.79
4786 elmex 1.85 my @tex =
4787 root 1.340 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
4788 root 1.381 qw(b1_button_inactive.png b1_button_active.png);
4789 elmex 1.85
4790 root 1.79 sub new {
4791     my $class = shift;
4792    
4793     $class->SUPER::new (
4794 root 1.258 padding_x => 4,
4795     padding_y => 4,
4796 root 1.381 fg => [1.0, 1.0, 1.0],
4797     active_fg => [0.8, 0.8, 0.8],
4798 root 1.164 can_hover => 1,
4799     align => 0,
4800     valign => 0,
4801 elmex 1.150 can_events => 1,
4802 root 1.79 @_
4803     )
4804     }
4805    
4806 root 1.305 sub invoke_button_up {
4807 root 1.79 my ($self, $ev, $x, $y) = @_;
4808    
4809 root 1.231 $self->emit ("activate")
4810     if $x >= 0 && $x < $self->{w}
4811     && $y >= 0 && $y < $self->{h};
4812 root 1.271
4813     1
4814 root 1.79 }
4815    
4816     sub _draw {
4817     my ($self) = @_;
4818    
4819 root 1.279 local $self->{fg} = $GRAB == $self ? $self->{active_fg} : $self->{fg};
4820 root 1.79
4821 root 1.119 glEnable GL_TEXTURE_2D;
4822 elmex 1.85 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
4823 root 1.119 glColor 0, 0, 0, 1;
4824 elmex 1.85
4825 root 1.381 my $tex = $tex[$GRAB == $self];
4826     $tex->draw_quad_alpha (0, 0, $self->{w}, $self->{h});
4827 elmex 1.85
4828     glDisable GL_TEXTURE_2D;
4829 root 1.79
4830     $self->SUPER::_draw;
4831     }
4832    
4833     #############################################################################
4834    
4835 root 1.340 package CFPlus::UI::CheckBox;
4836 root 1.86
4837 root 1.340 our @ISA = CFPlus::UI::DrawBG::;
4838 root 1.86
4839 elmex 1.102 my @tex =
4840 root 1.340 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
4841 elmex 1.102 qw(c1_checkbox_bg.png c1_checkbox_active.png);
4842    
4843 root 1.340 use CFPlus::OpenGL;
4844 root 1.86
4845     sub new {
4846     my $class = shift;
4847    
4848     $class->SUPER::new (
4849 root 1.258 padding_x => 2,
4850     padding_y => 2,
4851 root 1.86 fg => [1, 1, 1],
4852     active_fg => [1, 1, 0],
4853 root 1.209 bg => [0, 0, 0, 0.2],
4854     active_bg => [1, 1, 1, 0.5],
4855 root 1.86 state => 0,
4856 root 1.97 can_hover => 1,
4857 root 1.86 @_
4858     )
4859     }
4860    
4861 root 1.87 sub size_request {
4862     my ($self) = @_;
4863    
4864 root 1.258 (6) x 2
4865 root 1.87 }
4866    
4867 root 1.319 sub toggle {
4868     my ($self) = @_;
4869    
4870     $self->{state} = !$self->{state};
4871     $self->emit (changed => $self->{state});
4872     $self->update;
4873     }
4874    
4875 root 1.305 sub invoke_button_down {
4876 root 1.86 my ($self, $ev, $x, $y) = @_;
4877    
4878 root 1.258 if ($x >= $self->{padding_x} && $x < $self->{w} - $self->{padding_x}
4879     && $y >= $self->{padding_y} && $y < $self->{h} - $self->{padding_y}) {
4880 root 1.319 $self->toggle;
4881 root 1.271 } else {
4882     return 0
4883 root 1.86 }
4884 root 1.271
4885     1
4886 root 1.86 }
4887    
4888     sub _draw {
4889     my ($self) = @_;
4890    
4891 root 1.87 $self->SUPER::_draw;
4892 root 1.86
4893 root 1.258 glTranslate $self->{padding_x} + 0.375, $self->{padding_y} + 0.375, 0;
4894 root 1.86
4895 root 1.258 my ($w, $h) = @$self{qw(w h)};
4896    
4897     my $s = List::Util::min $w - $self->{padding_x} * 2, $h - $self->{padding_y} * 2;
4898 elmex 1.102
4899 root 1.87 glColor @{ $FOCUS == $self ? $self->{active_fg} : $self->{fg} };
4900 root 1.86
4901 elmex 1.102 my $tex = $self->{state} ? $tex[1] : $tex[0];
4902    
4903 root 1.197 glEnable GL_TEXTURE_2D;
4904 root 1.195 $tex->draw_quad_alpha (0, 0, $s, $s);
4905 elmex 1.102 glDisable GL_TEXTURE_2D;
4906 root 1.86 }
4907    
4908     #############################################################################
4909    
4910 root 1.340 package CFPlus::UI::Image;
4911 elmex 1.145
4912 root 1.340 our @ISA = CFPlus::UI::Base::;
4913 elmex 1.145
4914 root 1.340 use CFPlus::OpenGL;
4915 elmex 1.145
4916 root 1.310 our %texture_cache;
4917 elmex 1.145
4918     sub new {
4919     my $class = shift;
4920    
4921 root 1.310 my $self = $class->SUPER::new (
4922     can_events => 0,
4923     @_,
4924     );
4925 elmex 1.145
4926 root 1.327 $self->{path} || $self->{tex}
4927     or Carp::croak "'path' or 'tex' attributes required";
4928 elmex 1.145
4929 root 1.327 $self->{tex} ||= $texture_cache{$self->{path}} ||=
4930 root 1.340 new_from_file CFPlus::Texture CFPlus::find_rcfile $self->{path}, mipmap => 1;
4931 elmex 1.145
4932 root 1.362 CFPlus::weaken $texture_cache{$self->{path}};
4933 root 1.147
4934 root 1.310 $self->{aspect} ||= $self->{tex}{w} / $self->{tex}{h};
4935 elmex 1.145
4936     $self
4937     }
4938    
4939 root 1.334 sub STORABLE_freeze {
4940     my ($self, $cloning) = @_;
4941    
4942     $self->{path}
4943 root 1.340 or die "cannot serialise CFPlus::UI::Image on non-loadable images\n";
4944 root 1.334
4945     $self->{path}
4946     }
4947    
4948     sub STORABLE_attach {
4949     my ($self, $cloning, $path) = @_;
4950    
4951     $self->new (path => $path)
4952     }
4953    
4954 elmex 1.145 sub size_request {
4955     my ($self) = @_;
4956    
4957 root 1.310 ($self->{tex}{w}, $self->{tex}{h})
4958 elmex 1.145 }
4959    
4960     sub _draw {
4961     my ($self) = @_;
4962    
4963     my $tex = $self->{tex};
4964    
4965     my ($w, $h) = ($self->{w}, $self->{h});
4966    
4967     if ($self->{rot90}) {
4968     glRotate 90, 0, 0, 1;
4969     glTranslate 0, -$self->{w}, 0;
4970    
4971     ($w, $h) = ($h, $w);
4972     }
4973    
4974     glEnable GL_TEXTURE_2D;
4975     glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
4976    
4977 root 1.334 $tex->draw_quad (0, 0, $w, $h);
4978 elmex 1.145
4979     glDisable GL_TEXTURE_2D;
4980     }
4981    
4982     #############################################################################
4983    
4984 root 1.340 package CFPlus::UI::ImageButton;
4985 root 1.336
4986 root 1.340 our @ISA = CFPlus::UI::Image::;
4987 root 1.336
4988 root 1.340 use CFPlus::OpenGL;
4989 root 1.336
4990     my %textures;
4991    
4992     sub new {
4993     my $class = shift;
4994    
4995     my $self = $class->SUPER::new (
4996     padding_x => 4,
4997     padding_y => 4,
4998     fg => [1, 1, 1],
4999     active_fg => [0, 0, 1],
5000     can_hover => 1,
5001     align => 0,
5002     valign => 0,
5003     can_events => 1,
5004     @_
5005     );
5006     }
5007    
5008     sub invoke_button_up {
5009     my ($self, $ev, $x, $y) = @_;
5010    
5011     $self->emit ("activate")
5012     if $x >= 0 && $x < $self->{w}
5013     && $y >= 0 && $y < $self->{h};
5014    
5015     1
5016     }
5017    
5018     #############################################################################
5019    
5020 root 1.340 package CFPlus::UI::VGauge;
5021 elmex 1.124
5022 root 1.340 our @ISA = CFPlus::UI::Base::;
5023 elmex 1.124
5024 root 1.158 use List::Util qw(min max);
5025    
5026 root 1.340 use CFPlus::OpenGL;
5027 elmex 1.124
5028     my %tex = (
5029     food => [
5030 root 1.340 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
5031 elmex 1.124 qw/g1_food_gauge_empty.png g1_food_gauge_full.png/
5032     ],
5033     grace => [
5034 root 1.340 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
5035 root 1.158 qw/g1_grace_gauge_empty.png g1_grace_gauge_full.png g1_grace_gauge_overflow.png/
5036 elmex 1.124 ],
5037     hp => [
5038 root 1.340 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
5039 elmex 1.124 qw/g1_hp_gauge_empty.png g1_hp_gauge_full.png/
5040     ],
5041     mana => [
5042 root 1.340 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
5043 root 1.158 qw/g1_mana_gauge_empty.png g1_mana_gauge_full.png g1_mana_gauge_overflow.png/
5044 elmex 1.124 ],
5045     );
5046    
5047     # eg. VGauge->new (gauge => 'food'), default gauge: food
5048     sub new {
5049     my $class = shift;
5050    
5051 root 1.140 my $self = $class->SUPER::new (
5052 root 1.141 type => 'food',
5053 root 1.140 @_
5054     );
5055    
5056 root 1.141 $self->{aspect} = $tex{$self->{type}}[0]{w} / $tex{$self->{type}}[0]{h};
5057 elmex 1.124
5058     $self
5059     }
5060    
5061     sub size_request {
5062     my ($self) = @_;
5063    
5064 root 1.143 #my $tex = $tex{$self->{type}}[0];
5065     #@$tex{qw(w h)}
5066     (0, 0)
5067 elmex 1.124 }
5068    
5069     sub set_max {
5070     my ($self, $max) = @_;
5071 root 1.127
5072 root 1.173 return if $self->{max_val} == $max;
5073    
5074 elmex 1.124 $self->{max_val} = $max;
5075 root 1.173 $self->update;
5076 elmex 1.124 }
5077    
5078     sub set_value {
5079     my ($self, $val, $max) = @_;
5080    
5081     $self->set_max ($max)
5082     if defined $max;
5083    
5084 root 1.173 return if $self->{val} == $val;
5085    
5086 elmex 1.124 $self->{val} = $val;
5087     $self->update;
5088     }
5089    
5090     sub _draw {
5091     my ($self) = @_;
5092    
5093 root 1.141 my $tex = $tex{$self->{type}};
5094 root 1.158 my ($t1, $t2, $t3) = @$tex;
5095 elmex 1.124
5096     my ($w, $h) = ($self->{w}, $self->{h});
5097    
5098 elmex 1.142 if ($self->{vertical}) {
5099     glRotate 90, 0, 0, 1;
5100     glTranslate 0, -$self->{w}, 0;
5101    
5102     ($w, $h) = ($h, $w);
5103     }
5104    
5105 elmex 1.124 my $ycut = $self->{val} / ($self->{max_val} || 1);
5106    
5107 root 1.158 my $ycut1 = max 0, min 1, $ycut;
5108     my $ycut2 = max 0, min 1, $ycut - 1;
5109    
5110     my $h1 = $self->{h} * (1 - $ycut1);
5111     my $h2 = $self->{h} * (1 - $ycut2);
5112 root 1.317 my $h3 = $self->{h};
5113    
5114     $_ = $_ * (284-4)/288 + 4/288 for ($h1, $h2, $h3);
5115 elmex 1.124
5116     glEnable GL_BLEND;
5117 root 1.278 glBlendFuncSeparate GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
5118     GL_ONE, GL_ONE_MINUS_SRC_ALPHA;
5119 elmex 1.124 glEnable GL_TEXTURE_2D;
5120     glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
5121    
5122 root 1.131 glBindTexture GL_TEXTURE_2D, $t1->{name};
5123     glBegin GL_QUADS;
5124 root 1.158 glTexCoord 0 , 0; glVertex 0 , 0;
5125     glTexCoord 0 , $t1->{t} * (1 - $ycut1); glVertex 0 , $h1;
5126     glTexCoord $t1->{s}, $t1->{t} * (1 - $ycut1); glVertex $w, $h1;
5127     glTexCoord $t1->{s}, 0; glVertex $w, 0;
5128 root 1.131 glEnd;
5129 elmex 1.124
5130 root 1.158 my $ycut1 = List::Util::min 1, $ycut;
5131 root 1.131 glBindTexture GL_TEXTURE_2D, $t2->{name};
5132     glBegin GL_QUADS;
5133 root 1.158 glTexCoord 0 , $t2->{t} * (1 - $ycut1); glVertex 0 , $h1;
5134     glTexCoord 0 , $t2->{t} * (1 - $ycut2); glVertex 0 , $h2;
5135     glTexCoord $t2->{s}, $t2->{t} * (1 - $ycut2); glVertex $w, $h2;
5136     glTexCoord $t2->{s}, $t2->{t} * (1 - $ycut1); glVertex $w, $h1;
5137 root 1.131 glEnd;
5138 elmex 1.124
5139 root 1.158 if ($t3) {
5140     glBindTexture GL_TEXTURE_2D, $t3->{name};
5141     glBegin GL_QUADS;
5142     glTexCoord 0 , $t3->{t} * (1 - $ycut2); glVertex 0 , $h2;
5143 root 1.317 glTexCoord 0 , $t3->{t}; glVertex 0 , $h3;
5144     glTexCoord $t3->{s}, $t3->{t}; glVertex $w, $h3;
5145 root 1.158 glTexCoord $t3->{s}, $t3->{t} * (1 - $ycut2); glVertex $w, $h2;
5146     glEnd;
5147     }
5148    
5149 elmex 1.124 glDisable GL_BLEND;
5150     glDisable GL_TEXTURE_2D;
5151     }
5152    
5153     #############################################################################
5154    
5155 root 1.340 package CFPlus::UI::Gauge;
5156 root 1.141
5157 root 1.340 our @ISA = CFPlus::UI::VBox::;
5158 root 1.141
5159     sub new {
5160 root 1.151 my ($class, %arg) = @_;
5161 root 1.141
5162     my $self = $class->SUPER::new (
5163 root 1.171 tooltip => $arg{type},
5164     can_hover => 1,
5165     can_events => 1,
5166 root 1.151 %arg,
5167 root 1.141 );
5168    
5169 root 1.340 $self->add ($self->{value} = new CFPlus::UI::Label valign => +1, align => 0, template => "999");
5170     $self->add ($self->{gauge} = new CFPlus::UI::VGauge type => $self->{type}, expand => 1, can_hover => 1);
5171     $self->add ($self->{max} = new CFPlus::UI::Label valign => -1, align => 0, template => "999");
5172 root 1.141
5173     $self
5174     }
5175    
5176 elmex 1.146 sub set_fontsize {
5177     my ($self, $fsize) = @_;
5178    
5179     $self->{value}->set_fontsize ($fsize);
5180     $self->{max} ->set_fontsize ($fsize);
5181     }
5182    
5183 root 1.173 sub set_max {
5184     my ($self, $max) = @_;
5185    
5186     $self->{gauge}->set_max ($max);
5187     $self->{max}->set_text ($max);
5188     }
5189    
5190 root 1.141 sub set_value {
5191     my ($self, $val, $max) = @_;
5192    
5193     $self->set_max ($max)
5194     if defined $max;
5195    
5196     $self->{gauge}->set_value ($val, $max);
5197     $self->{value}->set_text ($val);
5198     }
5199    
5200     #############################################################################
5201    
5202 root 1.340 package CFPlus::UI::Slider;
5203 root 1.68
5204     use strict;
5205    
5206 root 1.340 use CFPlus::OpenGL;
5207 root 1.68
5208 root 1.340 our @ISA = CFPlus::UI::DrawBG::;
5209 root 1.68
5210 elmex 1.99 my @tex =
5211 root 1.340 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_ }
5212 elmex 1.99 qw(s1_slider.png s1_slider_bg.png);
5213    
5214 root 1.68 sub new {
5215     my $class = shift;
5216    
5217 root 1.206 # range [value, low, high, page, unit]
5218 root 1.68
5219 root 1.97 # TODO: 0-width page
5220     # TODO: req_w/h are wrong with vertical
5221     # TODO: calculations are off
5222 root 1.76 my $self = $class->SUPER::new (
5223 root 1.68 fg => [1, 1, 1],
5224     active_fg => [0, 0, 0],
5225 root 1.209 bg => [0, 0, 0, 0.2],
5226     active_bg => [1, 1, 1, 0.5],
5227 root 1.227 range => [0, 0, 100, 10, 0],
5228 root 1.257 min_w => $::WIDTH / 80,
5229     min_h => $::WIDTH / 80,
5230 root 1.76 vertical => 0,
5231 root 1.97 can_hover => 1,
5232 root 1.217 inner_pad => 0.02,
5233 root 1.68 @_
5234 root 1.76 );
5235    
5236 root 1.206 $self->set_value ($self->{range}[0]);
5237     $self->update;
5238    
5239 root 1.76 $self
5240     }
5241    
5242 root 1.225 sub set_range {
5243     my ($self, $range) = @_;
5244    
5245 root 1.239 ($range, $self->{range}) = ($self->{range}, $range);
5246 root 1.225
5247 root 1.295 if ("@$range" ne "@{$self->{range}}") {
5248     $self->update;
5249     $self->set_value ($self->{range}[0]);
5250     }
5251 root 1.225 }
5252    
5253 root 1.206 sub set_value {
5254     my ($self, $value) = @_;
5255    
5256     my ($old_value, $lo, $hi, $page, $unit) = @{$self->{range}};
5257    
5258     $hi = $lo + 1 if $hi <= $lo;
5259    
5260 root 1.227 $page = $hi - $lo if $page > $hi - $lo;
5261    
5262     $value = $lo if $value < $lo;
5263     $value = $hi - $page if $value > $hi - $page;
5264 root 1.206
5265     $value = $lo + $unit * int +($value - $lo + $unit * 0.5) / $unit
5266     if $unit;
5267    
5268     @{$self->{range}} = ($value, $lo, $hi, $page, $unit);
5269    
5270     if ($value != $old_value) {
5271 root 1.305 $self->emit (changed => $value);
5272 root 1.206 $self->update;
5273     }
5274     }
5275    
5276 root 1.76 sub size_request {
5277     my ($self) = @_;
5278    
5279 root 1.257 ($self->{req_w}, $self->{req_h})
5280 root 1.68 }
5281    
5282 root 1.305 sub invoke_button_down {
5283 root 1.69 my ($self, $ev, $x, $y) = @_;
5284    
5285 root 1.305 $self->SUPER::invoke_button_down ($ev, $x, $y);
5286 root 1.227
5287     $self->{click} = [$self->{range}[0], $self->{vertical} ? $y : $x];
5288    
5289 root 1.307 $self->invoke_mouse_motion ($ev, $x, $y)
5290 root 1.69 }
5291    
5292 root 1.305 sub invoke_mouse_motion {
5293 root 1.69 my ($self, $ev, $x, $y) = @_;
5294    
5295     if ($GRAB == $self) {
5296 root 1.71 my ($x, $w) = $self->{vertical} ? ($y, $self->{h}) : ($x, $self->{w});
5297    
5298 root 1.206 my (undef, $lo, $hi, $page) = @{$self->{range}};
5299 elmex 1.103
5300 root 1.227 $x = ($x - $self->{click}[1]) / ($w * $self->{scale});
5301 root 1.69
5302 root 1.227 $self->set_value ($self->{click}[0] + $x * ($hi - $page - $lo));
5303 root 1.271 } else {
5304     return 0;
5305 root 1.69 }
5306 root 1.271
5307     1
5308 root 1.69 }
5309    
5310 root 1.330 sub invoke_mouse_wheel {
5311     my ($self, $ev) = @_;
5312    
5313     my $delta = $self->{vertical} ? $ev->{dy} : $ev->{dx};
5314    
5315 root 1.364 my $pagepart = $ev->{mod} & CFPlus::KMOD_SHIFT ? 1 : 0.2;
5316    
5317     $self->set_value ($self->{range}[0] + $delta * $self->{range}[3] * $pagepart);
5318 root 1.330
5319     ! ! $delta
5320     }
5321    
5322 root 1.206 sub update {
5323     my ($self) = @_;
5324    
5325 root 1.275 delete $self->{knob_w};
5326     $self->SUPER::update;
5327     }
5328    
5329     sub _draw {
5330     my ($self) = @_;
5331    
5332     unless ($self->{knob_w}) {
5333 root 1.206 $self->set_value ($self->{range}[0]);
5334    
5335     my ($value, $lo, $hi, $page) = @{$self->{range}};
5336 root 1.227 my $range = ($hi - $page - $lo) || 1e-100;
5337 root 1.206
5338 root 1.227 my $knob_w = List::Util::min 1, $page / ($hi - $lo) || 0.1;
5339 root 1.206
5340 root 1.227 $self->{offset} = List::Util::max $self->{inner_pad}, $knob_w * 0.5;
5341     $self->{scale} = 1 - 2 * $self->{offset} || 1e-100;
5342 root 1.206
5343 root 1.227 $value = ($value - $lo) / $range;
5344     $value = $value * $self->{scale} + $self->{offset};
5345 root 1.206
5346 root 1.227 $self->{knob_x} = $value - $knob_w * 0.5;
5347     $self->{knob_w} = $knob_w;
5348 root 1.275 }
5349 root 1.68
5350     $self->SUPER::_draw ();
5351    
5352 root 1.206 glScale $self->{w}, $self->{h};
5353 root 1.68
5354     if ($self->{vertical}) {
5355     # draw a vertical slider like a rotated horizontal slider
5356    
5357 root 1.214 glTranslate 1, 0, 0;
5358 root 1.68 glRotate 90, 0, 0, 1;
5359     }
5360    
5361     my $fg = $FOCUS == $self ? $self->{active_fg} : $self->{fg};
5362     my $bg = $FOCUS == $self ? $self->{active_bg} : $self->{bg};
5363    
5364 elmex 1.99 glEnable GL_TEXTURE_2D;
5365     glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
5366    
5367     # draw background
5368 root 1.206 $tex[1]->draw_quad_alpha (0, 0, 1, 1);
5369 root 1.69
5370 elmex 1.99 # draw handle
5371 root 1.206 $tex[0]->draw_quad_alpha ($self->{knob_x}, 0, $self->{knob_w}, 1);
5372 root 1.69
5373 elmex 1.99 glDisable GL_TEXTURE_2D;
5374 root 1.51 }
5375    
5376 root 1.39 #############################################################################
5377    
5378 root 1.340 package CFPlus::UI::ValSlider;
5379 root 1.225
5380 root 1.340 our @ISA = CFPlus::UI::HBox::;
5381 root 1.225
5382     sub new {
5383     my ($class, %arg) = @_;
5384    
5385     my $range = delete $arg{range};
5386    
5387     my $self = $class->SUPER::new (
5388 root 1.340 slider => (new CFPlus::UI::Slider expand => 1, range => $range),
5389     entry => (new CFPlus::UI::Label text => "", template => delete $arg{template}),
5390 root 1.225 to_value => sub { shift },
5391     from_value => sub { shift },
5392     %arg,
5393     );
5394    
5395     $self->{slider}->connect (changed => sub {
5396     my ($self, $value) = @_;
5397     $self->{parent}{entry}->set_text ($self->{parent}{to_value}->($value));
5398     $self->{parent}->emit (changed => $value);
5399     });
5400    
5401     # $self->{entry}->connect (changed => sub {
5402     # my ($self, $value) = @_;
5403     # $self->{parent}{slider}->set_value ($self->{parent}{from_value}->($value));
5404     # $self->{parent}->emit (changed => $value);
5405     # });
5406    
5407     $self->add ($self->{slider}, $self->{entry});
5408    
5409     $self->{slider}->emit (changed => $self->{slider}{range}[0]);
5410    
5411     $self
5412     }
5413    
5414     sub set_range { shift->{slider}->set_range (@_) }
5415     sub set_value { shift->{slider}->set_value (@_) }
5416    
5417     #############################################################################
5418    
5419 root 1.340 package CFPlus::UI::TextScroller;
5420 root 1.97
5421 root 1.340 our @ISA = CFPlus::UI::HBox::;
5422 root 1.97
5423 root 1.340 use CFPlus::OpenGL;
5424 root 1.97
5425     sub new {
5426     my $class = shift;
5427    
5428     my $self = $class->SUPER::new (
5429 root 1.164 fontsize => 1,
5430 root 1.330 can_events => 1,
5431 root 1.293 indent => 0,
5432 root 1.164 #font => default_font
5433 root 1.105 @_,
5434 root 1.164
5435 root 1.340 layout => (new CFPlus::Layout),
5436 root 1.164 par => [],
5437 root 1.365 max_par => 0,
5438 root 1.164 height => 0,
5439     children => [
5440 root 1.340 (new CFPlus::UI::Empty expand => 1),
5441     (new CFPlus::UI::Slider vertical => 1),
5442 root 1.97 ],
5443     );
5444    
5445 root 1.176 $self->{children}[1]->connect (changed => sub { $self->update });
5446 root 1.107
5447 root 1.97 $self
5448     }
5449    
5450 root 1.107 sub set_fontsize {
5451     my ($self, $fontsize) = @_;
5452    
5453     $self->{fontsize} = $fontsize;
5454     $self->reflow;
5455     }
5456    
5457 root 1.312 sub size_request {
5458 root 1.310 my ($self) = @_;
5459    
5460 root 1.312 my ($empty, $slider) = @{ $self->{children} };
5461    
5462     local $self->{children} = [$empty, $slider];
5463     $self->SUPER::size_request
5464 root 1.310 }
5465    
5466 root 1.305 sub invoke_size_allocate {
5467 root 1.259 my ($self, $w, $h) = @_;
5468 root 1.220
5469 root 1.311 my ($empty, $slider, @other) = @{ $self->{children} };
5470 root 1.310 $_->configure (@$_{qw(x y req_w req_h)}) for @other;
5471    
5472 root 1.220 $self->{layout}->set_font ($self->{font}) if $self->{font};
5473     $self->{layout}->set_height ($self->{fontsize} * $::FONTSIZE);
5474 root 1.311 $self->{layout}->set_width ($empty->{w});
5475 root 1.293 $self->{layout}->set_indent ($self->{fontsize} * $::FONTSIZE * $self->{indent});
5476 root 1.220
5477     $self->reflow;
5478 root 1.305
5479 root 1.312 local $self->{children} = [$empty, $slider];
5480 root 1.305 $self->SUPER::invoke_size_allocate ($w, $h)
5481 root 1.220 }
5482    
5483 root 1.330 sub invoke_mouse_wheel {
5484     my ($self, $ev) = @_;
5485    
5486     return 0 unless $ev->{dy}; # only vertical movements
5487    
5488     $self->{children}[1]->emit (mouse_wheel => $ev);
5489    
5490     1
5491     }
5492    
5493 root 1.310 sub get_layout {
5494     my ($self, $para) = @_;
5495 root 1.105
5496     my $layout = $self->{layout};
5497    
5498 root 1.310 $layout->set_font ($self->{font}) if $self->{font};
5499 root 1.311 $layout->set_foreground (@{$para->{fg}});
5500 root 1.134 $layout->set_height ($self->{fontsize} * $::FONTSIZE);
5501 root 1.310 $layout->set_width ($self->{children}[0]{w} - $para->{indent});
5502 root 1.293 $layout->set_indent ($self->{fontsize} * $::FONTSIZE * $self->{indent});
5503 root 1.310 $layout->set_markup ($para->{markup});
5504 root 1.311
5505     $layout->set_shapes (
5506     map
5507     +(0, $_->baseline_shift +$_->{padding_y} - $_->{h}, $_->{w}, $_->{h}),
5508     @{$para->{widget}}
5509     );
5510 root 1.310
5511     $layout
5512 root 1.105 }
5513    
5514     sub reflow {
5515     my ($self) = @_;
5516    
5517 root 1.107 $self->{need_reflow}++;
5518     $self->update;
5519 root 1.105 }
5520    
5521 root 1.227 sub set_offset {
5522     my ($self, $offset) = @_;
5523    
5524     # todo: base offset on lines or so, not on pixels
5525     $self->{children}[1]->set_value ($offset);
5526     }
5527    
5528 root 1.345 sub current_paragraph {
5529     my ($self) = @_;
5530    
5531     $self->{top_paragraph} - 1
5532     }
5533    
5534     sub scroll_to {
5535     my ($self, $para) = @_;
5536    
5537     $para = List::Util::max 0, List::Util::min $#{$self->{par}}, $para;
5538    
5539 root 1.347 $self->{scroll_to} = $para;
5540     $self->update;
5541 root 1.345 }
5542    
5543 root 1.226 sub clear {
5544     my ($self) = @_;
5545    
5546 root 1.310 my (undef, undef, @other) = @{ $self->{children} };
5547     $self->remove ($_) for @other;
5548    
5549 root 1.226 $self->{par} = [];
5550     $self->{height} = 0;
5551 root 1.227 $self->{children}[1]->set_range ([0, 0, 0, 1, 1]);
5552 root 1.226 }
5553    
5554 root 1.97 sub add_paragraph {
5555 root 1.338 my $self = shift;
5556 root 1.310
5557 root 1.338 for my $para (@_) {
5558     $para = {
5559     fg => [1, 1, 1, 1],
5560     indent => 0,
5561     markup => "",
5562     widget => [],
5563     ref $para ? %$para : (markup => $para),
5564     w => 1e10,
5565     wrapped => 1,
5566     };
5567 root 1.310
5568 root 1.338 $self->add (@{ $para->{widget} }) if @{ $para->{widget} };
5569     push @{$self->{par}}, $para;
5570     }
5571 root 1.97
5572 root 1.365 if (my $max = $self->{max_par}) {
5573     shift @{$self->{par}} while @{$self->{par}} > $max;
5574     }
5575    
5576 root 1.310 $self->{need_reflow}++;
5577     $self->update;
5578     }
5579    
5580     sub scroll_to_bottom {
5581     my ($self) = @_;
5582 root 1.105
5583 root 1.347 $self->{scroll_to} = $#{$self->{par}};
5584 root 1.310 $self->update;
5585 root 1.97 }
5586    
5587 root 1.345 sub force_uptodate {
5588 root 1.97 my ($self) = @_;
5589    
5590 root 1.345 if (delete $self->{need_reflow}) {
5591     my ($W, $H) = @{$self->{children}[0]}{qw(w h)};
5592    
5593     my $height = 0;
5594    
5595     for my $para (@{$self->{par}}) {
5596     if ($para->{w} != $W && ($para->{wrapped} || $para->{w} > $W)) {
5597     my $layout = $self->get_layout ($para);
5598     my ($w, $h) = $layout->size;
5599    
5600     $para->{w} = $w + $para->{indent};
5601     $para->{h} = $h;
5602     $para->{wrapped} = $layout->has_wrapped;
5603     }
5604    
5605     $para->{y} = $height;
5606     $height += $para->{h};
5607     }
5608 root 1.105
5609 root 1.345 $self->{height} = $height;
5610     $self->{children}[1]->set_range ([$self->{children}[1]{range}[0], 0, $height, $H, 1]);
5611 root 1.228
5612 root 1.345 delete $self->{texture};
5613     }
5614 root 1.107
5615 root 1.347 if (my $paridx = delete $self->{scroll_to}) {
5616     $self->{children}[1]->set_value ($self->{par}[$paridx]{y});
5617 root 1.345 }
5618     }
5619 root 1.228
5620 root 1.345 sub update {
5621     my ($self) = @_;
5622 root 1.228
5623 root 1.345 $self->SUPER::update;
5624 root 1.107
5625 root 1.345 return unless $self->{h} > 0;
5626 root 1.107
5627 root 1.345 delete $self->{texture};
5628 root 1.295
5629 root 1.345 $ROOT->on_post_alloc ($self => sub {
5630     $self->force_uptodate;
5631 root 1.107
5632 root 1.345 my ($W, $H) = @{$self->{children}[0]}{qw(w h)};
5633 root 1.310
5634 root 1.340 $self->{texture} ||= new_from_opengl CFPlus::Texture $W, $H, sub {
5635 root 1.279 glClearColor 0, 0, 0, 0;
5636 root 1.107 glClear GL_COLOR_BUFFER_BIT;
5637    
5638 root 1.352 package CFPlus::UI::Base;
5639     local ($draw_x, $draw_y, $draw_w, $draw_h) =
5640     (0, 0, $self->{w}, $self->{h});
5641 root 1.351
5642 root 1.107 my $top = int $self->{children}[1]{range}[0];
5643 root 1.105
5644 root 1.347 my $paridx = 0;
5645     my $top_paragraph;
5646     my $top = int $self->{children}[1]{range}[0];
5647    
5648 root 1.107 my $y0 = $top;
5649 root 1.228 my $y1 = $top + $H;
5650 root 1.105
5651 root 1.310 for my $para (@{$self->{par}}) {
5652     my $h = $para->{h};
5653 root 1.345 my $y = $para->{y};
5654 root 1.97
5655 root 1.107 if ($y0 < $y + $h && $y < $y1) {
5656 root 1.310 my $layout = $self->get_layout ($para);
5657 root 1.220
5658 root 1.320 $layout->render ($para->{indent}, $y - $y0);
5659 root 1.310
5660     if (my @w = @{ $para->{widget} }) {
5661     my @s = $layout->get_shapes;
5662    
5663     for (@w) {
5664     my ($dx, $dy) = splice @s, 0, 2, ();
5665    
5666     $_->{x} = $dx + $para->{indent};
5667     $_->{y} = $dy + $y - $y0;
5668    
5669     $_->draw;
5670     }
5671     }
5672 root 1.107 }
5673 root 1.347
5674     $paridx++;
5675     $top_paragraph ||= $paridx if $y >= $top;
5676 root 1.105 }
5677 root 1.347
5678     $self->{top_paragraph} = $top_paragraph;
5679 root 1.107 };
5680     });
5681 root 1.105 }
5682 root 1.97
5683 root 1.310 sub reconfigure {
5684     my ($self) = @_;
5685    
5686     $self->SUPER::reconfigure;
5687    
5688     $_->{w} = 1e10 for @{ $self->{par} };
5689     $self->reflow;
5690     }
5691    
5692 root 1.105 sub _draw {
5693     my ($self) = @_;
5694 root 1.97
5695 root 1.176 glEnable GL_TEXTURE_2D;
5696     glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
5697 root 1.279 glColor 0, 0, 0, 1;
5698     $self->{texture}->draw_quad_alpha_premultiplied (0, 0, $self->{children}[0]{w}, $self->{children}[0]{h});
5699 root 1.176 glDisable GL_TEXTURE_2D;
5700 root 1.97
5701 root 1.106 $self->{children}[1]->draw;
5702 root 1.97 }
5703    
5704     #############################################################################
5705    
5706 root 1.340 package CFPlus::UI::Animator;
5707 root 1.35
5708 root 1.340 use CFPlus::OpenGL;
5709 root 1.35
5710 root 1.340 our @ISA = CFPlus::UI::Bin::;
5711 root 1.35
5712     sub moveto {
5713     my ($self, $x, $y) = @_;
5714    
5715     $self->{moveto} = [$self->{x}, $self->{y}, $x, $y];
5716 root 1.56 $self->{speed} = 0.001;
5717 root 1.35 $self->{time} = 1;
5718    
5719     ::animation_start $self;
5720     }
5721    
5722     sub animate {
5723     my ($self, $interval) = @_;
5724    
5725     $self->{time} -= $interval * $self->{speed};
5726     if ($self->{time} <= 0) {
5727     $self->{time} = 0;
5728     ::animation_stop $self;
5729     }
5730    
5731     my ($x0, $y0, $x1, $y1) = @{$self->{moveto}};
5732    
5733     $self->{x} = $x0 * $self->{time} + $x1 * (1 - $self->{time});
5734     $self->{y} = $y0 * $self->{time} + $y1 * (1 - $self->{time});
5735     }
5736    
5737     sub _draw {
5738     my ($self) = @_;
5739    
5740     glPushMatrix;
5741 root 1.51 glRotate $self->{time} * 1000, 0, 1, 0;
5742 root 1.38 $self->{children}[0]->draw;
5743 root 1.35 glPopMatrix;
5744     }
5745    
5746 root 1.51 #############################################################################
5747    
5748 root 1.340 package CFPlus::UI::Flopper;
5749 root 1.96
5750 root 1.340 our @ISA = CFPlus::UI::Button::;
5751 root 1.96
5752     sub new {
5753     my $class = shift;
5754    
5755     my $self = $class->SUPER::new (
5756 root 1.243 state => 0,
5757     on_activate => \&toggle_flopper,
5758 root 1.96 @_
5759     );
5760    
5761     $self
5762     }
5763    
5764     sub toggle_flopper {
5765     my ($self) = @_;
5766    
5767 elmex 1.245 $self->{other}->toggle_visibility;
5768 root 1.96 }
5769    
5770     #############################################################################
5771    
5772 root 1.340 package CFPlus::UI::Tooltip;
5773 root 1.153
5774 root 1.340 our @ISA = CFPlus::UI::Bin::;
5775 root 1.153
5776 root 1.340 use CFPlus::OpenGL;
5777 root 1.153
5778     sub new {
5779     my $class = shift;
5780    
5781     $class->SUPER::new (
5782     @_,
5783     can_events => 0,
5784     )
5785     }
5786    
5787 root 1.196 sub set_tooltip_from {
5788     my ($self, $widget) = @_;
5789 root 1.195
5790 root 1.342 $widget->{tooltip} = CFPlus::Pod::section_label tooltip => $1
5791 root 1.341 if $widget->{tooltip} =~ /^#(.*)$/;
5792    
5793 root 1.259 my $tooltip = $widget->{tooltip};
5794    
5795     if ($ENV{CFPLUS_DEBUG} & 2) {
5796     $tooltip .= "\n\n" . (ref $widget) . "\n"
5797     . "$widget->{x} $widget->{y} $widget->{w} $widget->{h}\n"
5798     . "req $widget->{req_w} $widget->{req_h}\n"
5799     . "visible $widget->{visible}";
5800     }
5801    
5802 root 1.298 $tooltip =~ s/^\n+//;
5803     $tooltip =~ s/\n+$//;
5804    
5805 root 1.340 $self->add (new CFPlus::UI::Label
5806 root 1.259 markup => $tooltip,
5807 root 1.213 max_w => ($widget->{tooltip_width} || 0.25) * $::WIDTH,
5808     fontsize => 0.8,
5809 root 1.343 style => 1, # FLAG_INVERSE
5810 root 1.213 ellipsise => 0,
5811     font => ($widget->{tooltip_font} || $::FONT_PROP),
5812 root 1.197 );
5813 root 1.153 }
5814    
5815     sub size_request {
5816     my ($self) = @_;
5817    
5818     my ($w, $h) = @{$self->child}{qw(req_w req_h)};
5819    
5820 root 1.154 ($w + 4, $h + 4)
5821     }
5822    
5823 root 1.305 sub invoke_size_allocate {
5824 root 1.259 my ($self, $w, $h) = @_;
5825 root 1.162
5826 root 1.305 $self->SUPER::invoke_size_allocate ($w - 4, $h - 4)
5827 root 1.162 }
5828    
5829 root 1.305 sub invoke_visibility_change {
5830 root 1.253 my ($self, $visible) = @_;
5831    
5832     return unless $visible;
5833    
5834     $self->{root}->on_post_alloc ("move_$self" => sub {
5835 root 1.254 my $widget = $self->{owner}
5836     or return;
5837 root 1.253
5838 root 1.332 if ($widget->{visible}) {
5839     my ($x, $y) = $widget->coord2global ($widget->{w}, 0);
5840 root 1.253
5841 root 1.332 ($x, $y) = $widget->coord2global (-$self->{w}, 0)
5842     if $x + $self->{w} > $self->{root}{w};
5843 root 1.253
5844 root 1.332 $self->move_abs ($x, $y);
5845     } else {
5846     $self->hide;
5847     }
5848 root 1.253 });
5849     }
5850    
5851 root 1.154 sub _draw {
5852     my ($self) = @_;
5853    
5854     glTranslate 0.375, 0.375;
5855    
5856     my ($w, $h) = @$self{qw(w h)};
5857    
5858     glColor 1, 0.8, 0.4;
5859     glBegin GL_QUADS;
5860     glVertex 0 , 0;
5861     glVertex 0 , $h;
5862     glVertex $w, $h;
5863     glVertex $w, 0;
5864     glEnd;
5865    
5866     glColor 0, 0, 0;
5867     glBegin GL_LINE_LOOP;
5868     glVertex 0 , 0;
5869     glVertex 0 , $h;
5870     glVertex $w, $h;
5871     glVertex $w, 0;
5872     glEnd;
5873    
5874 root 1.197 glTranslate 2 - 0.375, 2 - 0.375;
5875 root 1.252
5876 root 1.154 $self->SUPER::_draw;
5877 root 1.153 }
5878    
5879     #############################################################################
5880    
5881 root 1.340 package CFPlus::UI::Face;
5882 root 1.162
5883 root 1.340 our @ISA = CFPlus::UI::DrawBG::;
5884 root 1.162
5885 root 1.340 use CFPlus::OpenGL;
5886 root 1.162
5887     sub new {
5888     my $class = shift;
5889    
5890 root 1.217 my $self = $class->SUPER::new (
5891 root 1.373 size_w => 32,
5892     size_h => 8,
5893 root 1.234 aspect => 1,
5894     can_events => 0,
5895 root 1.162 @_,
5896 root 1.217 );
5897    
5898     if ($self->{anim} && $self->{animspeed}) {
5899 root 1.362 CFPlus::weaken (my $widget = $self);
5900 root 1.217
5901 root 1.379 $widget->{animspeed} = List::Util::max 0.05, $widget->{animspeed};
5902     $widget->{anim_start} = $self->{animspeed} * Event::time / $self->{animspeed};
5903 root 1.217 $self->{timer} = Event->timer (
5904 root 1.379 parked => 1,
5905 root 1.217 cb => sub {
5906 root 1.380 return unless $::CONN && $widget;
5907 root 1.379
5908 root 1.217 ++$widget->{frame};
5909 root 1.379 $widget->update_face;
5910     $widget->update;
5911    
5912     $widget->update_timer;
5913 root 1.217 },
5914     );
5915 root 1.373
5916     $self->update_face;
5917 root 1.379 $self->update_timer;
5918 root 1.217 }
5919    
5920     $self
5921 root 1.162 }
5922    
5923 root 1.379 sub update_timer {
5924     my ($self) = @_;
5925    
5926     return unless $self->{timer};
5927    
5928     if ($self->{visible}) {
5929     $self->{timer}->at (
5930     $self->{anim_start}
5931     + $self->{animspeed}
5932     * int 1.5 + (Event::time - $self->{anim_start}) / $self->{animspeed}
5933     );
5934     $self->{timer}->start;
5935     } else {
5936     $self->{timer}->stop;
5937     }
5938     }
5939    
5940 root 1.373 sub update_face {
5941     my ($self) = @_;
5942    
5943     return unless $::CONN;
5944    
5945     if (my $anim = $::CONN->{anim}[$self->{anim}]) {
5946     if ($anim && @$anim) {
5947     delete $self->{wait_face};
5948     $self->{face} = $anim->[ $self->{frame} % @$anim ];
5949     }
5950     }
5951     }
5952    
5953 root 1.162 sub size_request {
5954 root 1.373 my ($self) = @_;
5955    
5956     if ($::CONN) {
5957     if (my $faceid = $::CONN->{faceid}[$self->{face}]) {
5958     if (my $tex = $::CONN->{texture}[$faceid]) {
5959     return ($self->{size_w} || $tex->{w}, $self->{size_h} || $tex->{h});
5960     } else {
5961     $self->{wait_face} ||= $::CONN->connect_face_update ($faceid, sub {
5962     $self->realloc;
5963     });
5964     }
5965     }
5966     }
5967    
5968     ($self->{size_w} || 8, $self->{size_h} || 8)
5969 root 1.162 }
5970    
5971 root 1.222 sub update {
5972     my ($self) = @_;
5973    
5974     return unless $self->{visible};
5975    
5976     $self->SUPER::update;
5977     }
5978    
5979 root 1.379 sub invoke_visibility_change {
5980     my ($self) = @_;
5981    
5982     $self->update_timer;
5983    
5984     0
5985     }
5986    
5987 elmex 1.179 sub _draw {
5988 root 1.162 my ($self) = @_;
5989    
5990 root 1.227 return unless $::CONN;
5991 root 1.162
5992 root 1.337 $self->SUPER::_draw;
5993    
5994 root 1.373 my $faceid = $::CONN->{faceid}[$self->{face}]
5995 root 1.371 or return;
5996 root 1.217
5997 root 1.371 my $tex = $::CONN->{texture}[$faceid];
5998 root 1.217
5999 root 1.162 if ($tex) {
6000     glEnable GL_TEXTURE_2D;
6001     glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
6002 root 1.279 glColor 0, 0, 0, 1;
6003 root 1.195 $tex->draw_quad_alpha (0, 0, $self->{w}, $self->{h});
6004 root 1.162 glDisable GL_TEXTURE_2D;
6005     }
6006     }
6007    
6008 root 1.302 sub destroy {
6009 root 1.217 my ($self) = @_;
6010    
6011 root 1.379 (delete $self->{timer})->cancel
6012 root 1.217 if $self->{timer};
6013    
6014 root 1.302 $self->SUPER::destroy;
6015 root 1.217 }
6016    
6017 root 1.162 #############################################################################
6018    
6019 root 1.340 package CFPlus::UI::Buttonbar;
6020 root 1.272
6021 root 1.340 our @ISA = CFPlus::UI::HBox::;
6022 root 1.272
6023 root 1.353 # TODO: should actually wrap buttons and other goodies.
6024 root 1.272
6025     #############################################################################
6026    
6027 root 1.340 package CFPlus::UI::Menu;
6028 root 1.178
6029 root 1.346 our @ISA = CFPlus::UI::Toplevel::;
6030 root 1.178
6031 root 1.340 use CFPlus::OpenGL;
6032 root 1.178
6033     sub new {
6034     my $class = shift;
6035    
6036     my $self = $class->SUPER::new (
6037     items => [],
6038     z => 100,
6039     @_,
6040     );
6041    
6042 root 1.340 $self->add ($self->{vbox} = new CFPlus::UI::VBox);
6043 root 1.178
6044     for my $item (@{ $self->{items} }) {
6045 root 1.291 my ($widget, $cb, $tooltip) = @$item;
6046 root 1.178
6047     # handle various types of items, only text for now
6048     if (!ref $widget) {
6049 root 1.322 if ($widget =~ /\t/) {
6050     my ($left, $right) = split /\t/, $widget, 2;
6051    
6052 root 1.340 $widget = new CFPlus::UI::HBox
6053 root 1.322 can_hover => 1,
6054     can_events => 1,
6055     tooltip => $tooltip,
6056     children => [
6057 root 1.340 (new CFPlus::UI::Label markup => $left, expand => 1),
6058     (new CFPlus::UI::Label markup => $right, align => +1),
6059 root 1.322 ],
6060     ;
6061    
6062     } else {
6063 root 1.340 $widget = new CFPlus::UI::Label
6064 root 1.322 can_hover => 1,
6065     can_events => 1,
6066     markup => $widget,
6067     tooltip => $tooltip;
6068     }
6069 root 1.178 }
6070    
6071     $self->{item}{$widget} = $item;
6072    
6073     $self->{vbox}->add ($widget);
6074     }
6075    
6076     $self
6077     }
6078    
6079     # popup given the event (must be a mouse button down event currently)
6080     sub popup {
6081     my ($self, $ev) = @_;
6082    
6083 root 1.305 $self->emit ("popdown");
6084 root 1.178
6085     # maybe save $GRAB? must be careful about events...
6086     $GRAB = $self;
6087     $self->{button} = $ev->{button};
6088    
6089     $self->show;
6090 root 1.258 $self->move_abs ($ev->{x} - $self->{w} * 0.5, $ev->{y} - $self->{h} * 0.5);
6091 root 1.178 }
6092    
6093 root 1.305 sub invoke_mouse_motion {
6094 root 1.178 my ($self, $ev, $x, $y) = @_;
6095    
6096 root 1.182 # TODO: should use vbox->find_widget or so
6097 root 1.178 $HOVER = $ROOT->find_widget ($ev->{x}, $ev->{y});
6098     $self->{hover} = $self->{item}{$HOVER};
6099 root 1.271
6100     0
6101 root 1.178 }
6102    
6103 root 1.305 sub invoke_button_up {
6104 root 1.178 my ($self, $ev, $x, $y) = @_;
6105    
6106     if ($ev->{button} == $self->{button}) {
6107     undef $GRAB;
6108     $self->hide;
6109    
6110 root 1.305 $self->emit ("popdown");
6111 root 1.178 $self->{hover}[1]->() if $self->{hover};
6112 root 1.271 } else {
6113     return 0
6114 root 1.178 }
6115 root 1.271
6116     1
6117 root 1.178 }
6118    
6119     #############################################################################
6120    
6121 root 1.340 package CFPlus::UI::Multiplexer;
6122 root 1.272
6123 root 1.340 our @ISA = CFPlus::UI::Container::;
6124 root 1.272
6125     sub new {
6126     my $class = shift;
6127    
6128     my $self = $class->SUPER::new (
6129     @_,
6130     );
6131    
6132     $self->{current} = $self->{children}[0]
6133     if @{ $self->{children} };
6134    
6135     $self
6136     }
6137    
6138     sub add {
6139     my ($self, @widgets) = @_;
6140    
6141     $self->SUPER::add (@widgets);
6142    
6143     $self->{current} = $self->{children}[0]
6144     if @{ $self->{children} };
6145     }
6146    
6147 root 1.319 sub get_current_page {
6148     my ($self) = @_;
6149    
6150     $self->{current}
6151     }
6152    
6153 root 1.272 sub set_current_page {
6154     my ($self, $page_or_widget) = @_;
6155    
6156     my $widget = ref $page_or_widget
6157     ? $page_or_widget
6158     : $self->{children}[$page_or_widget];
6159    
6160     $self->{current} = $widget;
6161     $self->{current}->configure (0, 0, $self->{w}, $self->{h});
6162    
6163 root 1.305 $self->emit (page_changed => $self->{current});
6164 root 1.272
6165     $self->realloc;
6166     }
6167    
6168     sub visible_children {
6169     $_[0]{current}
6170     }
6171    
6172     sub size_request {
6173     my ($self) = @_;
6174    
6175     $self->{current}->size_request
6176     }
6177    
6178 root 1.305 sub invoke_size_allocate {
6179 root 1.272 my ($self, $w, $h) = @_;
6180    
6181     $self->{current}->configure (0, 0, $w, $h);
6182 root 1.305
6183     1
6184 root 1.272 }
6185    
6186     sub _draw {
6187     my ($self) = @_;
6188    
6189     $self->{current}->draw;
6190     }
6191    
6192     #############################################################################
6193    
6194 root 1.340 package CFPlus::UI::Notebook;
6195 root 1.272
6196 root 1.340 our @ISA = CFPlus::UI::VBox::;
6197 root 1.272
6198     sub new {
6199     my $class = shift;
6200    
6201     my $self = $class->SUPER::new (
6202 root 1.340 buttonbar => (new CFPlus::UI::Buttonbar),
6203     multiplexer => (new CFPlus::UI::Multiplexer expand => 1),
6204 root 1.273 # filter => # will be put between multiplexer and $self
6205 root 1.272 @_,
6206     );
6207 root 1.273
6208     $self->{filter}->add ($self->{multiplexer}) if $self->{filter};
6209     $self->SUPER::add ($self->{buttonbar}, $self->{filter} || $self->{multiplexer});
6210 root 1.272
6211     $self
6212     }
6213    
6214     sub add {
6215     my ($self, $title, $widget, $tooltip) = @_;
6216    
6217 root 1.362 CFPlus::weaken $self;
6218 root 1.272
6219 root 1.340 $self->{buttonbar}->add (new CFPlus::UI::Button
6220 root 1.272 markup => $title,
6221     tooltip => $tooltip,
6222     on_activate => sub { $self->set_current_page ($widget) },
6223     );
6224    
6225     $self->{multiplexer}->add ($widget);
6226     }
6227    
6228 root 1.319 sub get_current_page {
6229     my ($self) = @_;
6230    
6231     $self->{multiplexer}->get_current_page
6232     }
6233    
6234 root 1.272 sub set_current_page {
6235     my ($self, $page) = @_;
6236    
6237     $self->{multiplexer}->set_current_page ($page);
6238 root 1.305 $self->emit (page_changed => $self->{multiplexer}{current});
6239 root 1.272 }
6240    
6241     #############################################################################
6242    
6243 root 1.340 package CFPlus::UI::Selector;
6244 root 1.291
6245     use utf8;
6246    
6247 root 1.340 our @ISA = CFPlus::UI::Button::;
6248 root 1.291
6249     sub new {
6250     my $class = shift;
6251    
6252     my $self = $class->SUPER::new (
6253 root 1.297 options => [], # [value, title, longdesc], ...
6254 root 1.291 value => undef,
6255     @_,
6256     );
6257    
6258     $self->_set_value ($self->{value});
6259    
6260     $self
6261     }
6262    
6263 root 1.305 sub invoke_button_down {
6264 root 1.291 my ($self, $ev) = @_;
6265    
6266     my @menu_items;
6267    
6268     for (@{ $self->{options} }) {
6269 root 1.297 my ($value, $title, $tooltip) = @$_;
6270 root 1.291
6271 root 1.297 push @menu_items, [$tooltip || $title, sub { $self->set_value ($value) }];
6272 root 1.291 }
6273    
6274 root 1.340 CFPlus::UI::Menu->new (items => \@menu_items)->popup ($ev);
6275 root 1.291 }
6276    
6277     sub _set_value {
6278     my ($self, $value) = @_;
6279    
6280 root 1.297 my ($item) = grep $_->[0] eq $value, @{ $self->{options} }
6281 root 1.291 or return;
6282    
6283 root 1.297 $self->{value} = $item->[0];
6284     $self->set_markup ("$item->[1] ⇓");
6285 root 1.291 $self->set_tooltip ($item->[2]);
6286     }
6287    
6288     sub set_value {
6289     my ($self, $value) = @_;
6290    
6291     return unless $self->{value} ne $value;
6292    
6293     $self->_set_value ($value);
6294 root 1.305 $self->emit (changed => $value);
6295 root 1.291 }
6296    
6297     #############################################################################
6298    
6299 root 1.340 package CFPlus::UI::Statusbox;
6300 root 1.194
6301 root 1.340 our @ISA = CFPlus::UI::VBox::;
6302 root 1.194
6303 root 1.210 sub new {
6304     my $class = shift;
6305    
6306 root 1.280 my $self = $class->SUPER::new (
6307 root 1.210 fontsize => 0.8,
6308     @_,
6309 root 1.280 );
6310    
6311 root 1.362 CFPlus::weaken (my $this = $self);
6312 root 1.280
6313 root 1.281 $self->{timer} = Event->timer (after => 1, interval => 1, cb => sub { $this->reorder });
6314 root 1.280
6315     $self
6316 root 1.210 }
6317    
6318 root 1.194 sub reorder {
6319     my ($self) = @_;
6320 root 1.280 my $NOW = Time::HiRes::time;
6321 root 1.194
6322 root 1.281 # freeze display when hovering over any label
6323 root 1.340 return if $CFPlus::UI::TOOLTIP->{owner}
6324     && grep $CFPlus::UI::TOOLTIP->{owner} == $_->{label},
6325 root 1.281 values %{ $self->{item} };
6326    
6327 root 1.194 while (my ($k, $v) = each %{ $self->{item} }) {
6328     delete $self->{item}{$k} if $v->{timeout} < $NOW;
6329     }
6330    
6331     my @widgets;
6332 root 1.197
6333     my @items = sort {
6334     $a->{pri} <=> $b->{pri}
6335     or $b->{id} <=> $a->{id}
6336     } values %{ $self->{item} };
6337    
6338 root 1.280 $self->{timer}->interval (1);
6339    
6340 root 1.194 my $count = 10 + 1;
6341     for my $item (@items) {
6342     last unless --$count;
6343    
6344 root 1.281 my $label = $item->{label} ||= do {
6345 root 1.194 # TODO: doesn't handle markup well (read as: at all)
6346 root 1.197 my $short = $item->{count} > 1
6347     ? "<b>$item->{count} ×</b> $item->{text}"
6348     : $item->{text};
6349    
6350 root 1.194 for ($short) {
6351     s/^\s+//;
6352 root 1.205 s/\s+/ /g;
6353 root 1.194 }
6354    
6355 root 1.340 new CFPlus::UI::Label
6356 root 1.196 markup => $short,
6357 root 1.197 tooltip => $item->{tooltip},
6358 root 1.196 tooltip_font => $::FONT_PROP,
6359 root 1.197 tooltip_width => 0.67,
6360 root 1.213 fontsize => $item->{fontsize} || $self->{fontsize},
6361     max_w => $::WIDTH * 0.44,
6362 root 1.281 fg => [@{ $item->{fg} }],
6363 root 1.196 can_events => 1,
6364 root 1.197 can_hover => 1
6365 root 1.194 };
6366 root 1.280
6367     if ((my $diff = $item->{timeout} - $NOW) < 2) {
6368 root 1.281 $label->{fg}[3] = ($item->{fg}[3] || 1) * $diff / 2;
6369     $label->update;
6370     $label->set_max_size (undef, $label->{req_h} * $diff)
6371     if $diff < 1;
6372 root 1.280 $self->{timer}->interval (1/30);
6373 root 1.281 } else {
6374     $label->{fg}[3] = $item->{fg}[3] || 1;
6375 root 1.280 }
6376 root 1.281
6377     push @widgets, $label;
6378 root 1.194 }
6379    
6380     $self->clear;
6381 root 1.197 $self->SUPER::add (reverse @widgets);
6382 root 1.194 }
6383    
6384     sub add {
6385     my ($self, $text, %arg) = @_;
6386    
6387 root 1.198 $text =~ s/^\s+//;
6388     $text =~ s/\s+$//;
6389    
6390 root 1.233 return unless $text;
6391    
6392 root 1.280 my $timeout = (int time) + ((delete $arg{timeout}) || 60);
6393 root 1.194
6394 root 1.197 my $group = exists $arg{group} ? $arg{group} : ++$self->{id};
6395 root 1.194
6396 root 1.197 if (my $item = $self->{item}{$group}) {
6397     if ($item->{text} eq $text) {
6398     $item->{count}++;
6399     } else {
6400     $item->{count} = 1;
6401     $item->{text} = $item->{tooltip} = $text;
6402     }
6403 root 1.300 $item->{id} += 0.2;#d#
6404 root 1.197 $item->{timeout} = $timeout;
6405     delete $item->{label};
6406     } else {
6407     $self->{item}{$group} = {
6408     id => ++$self->{id},
6409     text => $text,
6410     timeout => $timeout,
6411     tooltip => $text,
6412 root 1.205 fg => [0.8, 0.8, 0.8, 0.8],
6413 root 1.197 pri => 0,
6414     count => 1,
6415     %arg,
6416     };
6417     }
6418 root 1.194
6419 root 1.315 $ROOT->on_refresh (reorder => sub {
6420     $self->reorder;
6421     });
6422 root 1.194 }
6423    
6424 root 1.213 sub reconfigure {
6425     my ($self) = @_;
6426    
6427     delete $_->{label}
6428     for values %{ $self->{item} || {} };
6429    
6430     $self->reorder;
6431     $self->SUPER::reconfigure;
6432     }
6433    
6434 root 1.302 sub destroy {
6435 root 1.280 my ($self) = @_;
6436    
6437     $self->{timer}->cancel;
6438    
6439 root 1.302 $self->SUPER::destroy;
6440 root 1.280 }
6441    
6442 root 1.194 #############################################################################
6443    
6444 root 1.340 package CFPlus::UI::Root;
6445 root 1.265
6446 root 1.340 our @ISA = CFPlus::UI::Container::;
6447 elmex 1.260
6448 root 1.280 use List::Util qw(min max);
6449    
6450 root 1.340 use CFPlus::OpenGL;
6451 elmex 1.260
6452     sub new {
6453     my $class = shift;
6454    
6455 root 1.265 my $self = $class->SUPER::new (
6456     visible => 1,
6457     @_,
6458     );
6459    
6460 root 1.362 CFPlus::weaken ($self->{root} = $self);
6461 root 1.265
6462     $self
6463     }
6464    
6465     sub size_request {
6466     my ($self) = @_;
6467    
6468     ($self->{w}, $self->{h})
6469     }
6470 elmex 1.260
6471 root 1.265 sub _to_pixel {
6472     my ($coord, $size, $max) = @_;
6473 elmex 1.260
6474 root 1.265 $coord =
6475     $coord eq "center" ? ($max - $size) * 0.5
6476     : $coord eq "max" ? $max
6477     : $coord;
6478 elmex 1.260
6479 root 1.265 $coord = 0 if $coord < 0;
6480     $coord = $max - $size if $coord > $max - $size;
6481 elmex 1.260
6482 root 1.265 int $coord + 0.5
6483     }
6484 elmex 1.260
6485 root 1.305 sub invoke_size_allocate {
6486 root 1.265 my ($self, $w, $h) = @_;
6487 elmex 1.261
6488 root 1.265 for my $child ($self->children) {
6489     my ($X, $Y, $W, $H) = @$child{qw(x y req_w req_h)};
6490 elmex 1.260
6491 root 1.265 $X = $child->{force_x} if exists $child->{force_x};
6492     $Y = $child->{force_y} if exists $child->{force_y};
6493 elmex 1.260
6494 root 1.265 $X = _to_pixel $X, $W, $self->{w};
6495     $Y = _to_pixel $Y, $H, $self->{h};
6496 elmex 1.260
6497 root 1.265 $child->configure ($X, $Y, $W, $H);
6498     }
6499 root 1.305
6500     1
6501 elmex 1.260 }
6502    
6503 root 1.265 sub coord2local {
6504     my ($self, $x, $y) = @_;
6505    
6506     ($x, $y)
6507 elmex 1.260 }
6508    
6509 root 1.265 sub coord2global {
6510     my ($self, $x, $y) = @_;
6511 elmex 1.260
6512 root 1.265 ($x, $y)
6513 elmex 1.260 }
6514    
6515 root 1.265 sub update {
6516 elmex 1.260 my ($self) = @_;
6517    
6518 root 1.265 $::WANT_REFRESH++;
6519     }
6520 elmex 1.260
6521 root 1.265 sub add {
6522     my ($self, @children) = @_;
6523 elmex 1.260
6524 root 1.265 $_->{is_toplevel} = 1
6525     for @children;
6526 elmex 1.260
6527 root 1.265 $self->SUPER::add (@children);
6528 elmex 1.260 }
6529    
6530 root 1.265 sub remove {
6531     my ($self, @children) = @_;
6532    
6533     $self->SUPER::remove (@children);
6534 elmex 1.260
6535 root 1.265 delete $self->{is_toplevel}
6536     for @children;
6537 elmex 1.260
6538 root 1.265 while (@children) {
6539     my $w = pop @children;
6540     push @children, $w->children;
6541     $w->set_invisible;
6542     }
6543     }
6544 elmex 1.260
6545 root 1.265 sub on_refresh {
6546     my ($self, $id, $cb) = @_;
6547 elmex 1.260
6548 root 1.265 $self->{refresh_hook}{$id} = $cb;
6549 elmex 1.260 }
6550    
6551 root 1.265 sub on_post_alloc {
6552     my ($self, $id, $cb) = @_;
6553    
6554     $self->{post_alloc_hook}{$id} = $cb;
6555 elmex 1.262 }
6556    
6557 root 1.265 sub draw {
6558 elmex 1.260 my ($self) = @_;
6559    
6560 root 1.265 while ($self->{refresh_hook}) {
6561     $_->()
6562     for values %{delete $self->{refresh_hook}};
6563     }
6564    
6565     if ($self->{realloc}) {
6566 root 1.266 my %queue;
6567 root 1.265 my @queue;
6568 root 1.266 my $widget;
6569 root 1.265
6570 root 1.266 outer:
6571 root 1.265 while () {
6572 root 1.266 if (my $realloc = delete $self->{realloc}) {
6573     for $widget (values %$realloc) {
6574     $widget->{visible} or next; # do not resize invisible widgets
6575 root 1.265
6576 root 1.266 $queue{$widget+0}++ and next; # duplicates are common
6577 root 1.265
6578 root 1.266 push @{ $queue[$widget->{visible}] }, $widget;
6579     }
6580 root 1.265 }
6581    
6582 root 1.266 while () {
6583     @queue or last outer;
6584    
6585     $widget = pop @{ $queue[-1] || [] }
6586     and last;
6587    
6588     pop @queue;
6589     }
6590 root 1.265
6591 root 1.266 delete $queue{$widget+0};
6592 root 1.265
6593     my ($w, $h) = $widget->size_request;
6594    
6595 root 1.280 $w = max $widget->{min_w}, $w + $widget->{padding_x} * 2;
6596     $h = max $widget->{min_h}, $h + $widget->{padding_y} * 2;
6597    
6598     $w = min $widget->{max_w}, $w if exists $widget->{max_w};
6599     $h = min $widget->{max_h}, $h if exists $widget->{max_h};
6600 root 1.265
6601     $w = $widget->{force_w} if exists $widget->{force_w};
6602     $h = $widget->{force_h} if exists $widget->{force_h};
6603    
6604     if ($widget->{req_w} != $w || $widget->{req_h} != $h
6605     || delete $widget->{force_realloc}) {
6606     $widget->{req_w} = $w;
6607     $widget->{req_h} = $h;
6608    
6609     $self->{size_alloc}{$widget+0} = $widget;
6610    
6611     if (my $parent = $widget->{parent}) {
6612 root 1.266 $self->{realloc}{$parent+0} = $parent
6613     unless $queue{$parent+0};
6614    
6615 root 1.265 $parent->{force_size_alloc} = 1;
6616     $self->{size_alloc}{$parent+0} = $parent;
6617     }
6618     }
6619    
6620     delete $self->{realloc}{$widget+0};
6621     }
6622     }
6623 elmex 1.260
6624 root 1.265 while (my $size_alloc = delete $self->{size_alloc}) {
6625     my @queue = sort { $b->{visible} <=> $a->{visible} }
6626     values %$size_alloc;
6627 elmex 1.260
6628 root 1.265 while () {
6629     my $widget = pop @queue || last;
6630 elmex 1.260
6631 root 1.265 my ($w, $h) = @$widget{qw(alloc_w alloc_h)};
6632 elmex 1.260
6633 root 1.265 $w = 0 if $w < 0;
6634     $h = 0 if $h < 0;
6635 elmex 1.260
6636 root 1.326 $w = max $widget->{min_w}, $w;
6637     $h = max $widget->{min_h}, $h;
6638    
6639 root 1.337 # $w = min $self->{w} - $widget->{x}, $w if $self->{w};
6640     # $h = min $self->{h} - $widget->{y}, $h if $self->{h};
6641    
6642 root 1.326 $w = min $widget->{max_w}, $w if exists $widget->{max_w};
6643     $h = min $widget->{max_h}, $h if exists $widget->{max_h};
6644    
6645 root 1.265 $w = int $w + 0.5;
6646     $h = int $h + 0.5;
6647 elmex 1.260
6648 root 1.265 if ($widget->{w} != $w || $widget->{h} != $h || delete $widget->{force_size_alloc}) {
6649 root 1.266 $widget->{old_w} = $widget->{w};
6650     $widget->{old_h} = $widget->{h};
6651    
6652 root 1.265 $widget->{w} = $w;
6653     $widget->{h} = $h;
6654 elmex 1.260
6655 root 1.265 $widget->emit (size_allocate => $w, $h);
6656     }
6657     }
6658     }
6659 elmex 1.260
6660 root 1.265 while ($self->{post_alloc_hook}) {
6661     $_->()
6662     for values %{delete $self->{post_alloc_hook}};
6663 elmex 1.260 }
6664 root 1.265
6665     glViewport 0, 0, $::WIDTH, $::HEIGHT;
6666     glClearColor +($::CFG->{fow_intensity}) x 3, 1;
6667     glClear GL_COLOR_BUFFER_BIT;
6668    
6669     glMatrixMode GL_PROJECTION;
6670     glLoadIdentity;
6671     glOrtho 0, $::WIDTH, $::HEIGHT, 0, -10000, 10000;
6672     glMatrixMode GL_MODELVIEW;
6673     glLoadIdentity;
6674    
6675 root 1.267 {
6676 root 1.340 package CFPlus::UI::Base;
6677 root 1.267
6678 root 1.351 local ($draw_x, $draw_y, $draw_w, $draw_h) =
6679 root 1.267 (0, 0, $self->{w}, $self->{h});
6680 root 1.352
6681     $self->_draw;
6682 root 1.267 }
6683 elmex 1.260 }
6684    
6685 elmex 1.262 #############################################################################
6686    
6687 root 1.340 package CFPlus::UI;
6688 root 1.51
6689 root 1.340 $ROOT = new CFPlus::UI::Root;
6690     $TOOLTIP = new CFPlus::UI::Tooltip z => 900;
6691 root 1.51
6692     1
6693 root 1.5