ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/Deliantra-Client/DC/UI.pm
(Generate patch)

Comparing deliantra/Deliantra-Client/DC/UI.pm (file contents):
Revision 1.115 by root, Sat Apr 15 19:35:20 2006 UTC vs.
Revision 1.384 by root, Fri Jul 20 16:32:11 2007 UTC

1package CFClient::UI; 1package CFPlus::UI;
2 2
3use utf8;
3use strict; 4use strict;
4 5
5use Scalar::Util ();
6use List::Util (); 6use List::Util ();
7use Event;
7 8
8use CFClient; 9use CFPlus;
10use CFPlus::Pod;
11use CFPlus::Texture;
9 12
10our ($FOCUS, $HOVER, $GRAB); # various widgets 13our ($FOCUS, $HOVER, $GRAB); # various widgets
11 14
15our $LAYOUT;
12our $ROOT; 16our $ROOT;
17our $TOOLTIP;
13our $BUTTON_STATE; 18our $BUTTON_STATE;
19
20our %WIDGET; # all widgets, weak-referenced
21
22our $TOOLTIP_WATCHER = Event->idle (min => 1/60, cb => sub {
23 if (!$GRAB) {
24 for (my $widget = $HOVER; $widget; $widget = $widget->{parent}) {
25 if (length $widget->{tooltip}) {
26 if ($TOOLTIP->{owner} != $widget) {
27 $TOOLTIP->{owner}->emit ("tooltip_hide") if $TOOLTIP->{owner};
28 $TOOLTIP->hide;
29
30 $TOOLTIP->{owner} = $widget;
31 $TOOLTIP->{owner}->emit ("tooltip_show") if $TOOLTIP->{owner};
32
33 return if $ENV{CFPLUS_DEBUG} & 8;
34
35 my $tip = $widget->{tooltip};
36
37 $tip = $tip->($widget) if CODE:: eq ref $tip;
38
39 $TOOLTIP->set_tooltip_from ($widget);
40 $TOOLTIP->show;
41 }
42
43 return;
44 }
45 }
46 }
47
48 $TOOLTIP->hide;
49 $TOOLTIP->{owner}->emit ("tooltip_hide") if $TOOLTIP->{owner};
50 delete $TOOLTIP->{owner};
51});
52
53sub 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
70sub set_layout {
71 my ($layout) = @_;
72
73 $LAYOUT = $layout;
74}
14 75
15# class methods for events 76# class methods for events
16sub feed_sdl_key_down_event { 77sub feed_sdl_key_down_event {
17 $FOCUS->key_down ($_[0]) if $FOCUS; 78 $FOCUS->emit (key_down => $_[0])
79 if $FOCUS;
18} 80}
19 81
20sub feed_sdl_key_up_event { 82sub feed_sdl_key_up_event {
21 $FOCUS->key_up ($_[0]) if $FOCUS; 83 $FOCUS->emit (key_up => $_[0])
84 if $FOCUS;
85}
86
87sub 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 }
22} 98}
23 99
24sub feed_sdl_button_down_event { 100sub feed_sdl_button_down_event {
25 my ($ev) = @_; 101 my ($ev) = @_;
26 my ($x, $y) = ($ev->motion_x, $ev->motion_y); 102 my ($x, $y) = ($ev->{x}, $ev->{y});
27 103
28 if (!$BUTTON_STATE) { 104 $BUTTON_STATE |= 1 << ($ev->{button} - 1);
105
106 unless ($GRAB) {
29 my $widget = $ROOT->find_widget ($x, $y); 107 my $widget = $ROOT->find_widget ($x, $y);
30 108
31 $GRAB = $widget; 109 $GRAB = $widget;
32 $GRAB->update if $GRAB; 110 $GRAB->update if $GRAB;
33 }
34 111
35 $BUTTON_STATE |= 1 << ($ev->button - 1); 112 $TOOLTIP_WATCHER->cb->();
113 }
36 114
37 $GRAB->button_down ($ev, $GRAB->coord2local ($x, $y)) if $GRAB; 115 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 }
38} 125}
39 126
40sub feed_sdl_button_up_event { 127sub feed_sdl_button_up_event {
41 my ($ev) = @_; 128 my ($ev) = @_;
42 my ($x, $y) = ($ev->motion_x, $ev->motion_y);
43 129
44 my $widget = $GRAB || $ROOT->find_widget ($x, $y); 130 my $widget = $GRAB || $ROOT->find_widget ($ev->{x}, $ev->{y});
45 131
46 $BUTTON_STATE &= ~(1 << ($ev->button - 1)); 132 $BUTTON_STATE &= ~(1 << ($ev->{button} - 1));
47 133
48 $GRAB->button_up ($ev, $GRAB->coord2local ($x, $y)) if $GRAB; 134 $GRAB->emit (button_up => $ev)
135 if $GRAB && $ev->{button} != 4 && $ev->{button} != 5;
49 136
50 if (!$BUTTON_STATE) { 137 unless ($BUTTON_STATE) {
51 my $grab = $GRAB; undef $GRAB; 138 my $grab = $GRAB; undef $GRAB;
52 $grab->update if $grab; 139 $grab->update if $grab;
53 $GRAB->update if $GRAB; 140 $GRAB->update if $GRAB;
141
142 check_hover $widget;
143 $TOOLTIP_WATCHER->cb->();
54 } 144 }
55} 145}
56 146
57sub feed_sdl_motion_event { 147sub feed_sdl_motion_event {
58 my ($ev) = @_; 148 my ($ev) = @_;
59 my ($x, $y) = ($ev->motion_x, $ev->motion_y); 149 my ($x, $y) = ($ev->{x}, $ev->{y});
60 150
61 my $widget = $GRAB || $ROOT->find_widget ($x, $y); 151 my $widget = $GRAB || $ROOT->find_widget ($x, $y);
62 152
63 if ($widget != $HOVER) { 153 check_hover $widget;
64 my $hover = $HOVER; $HOVER = $widget;
65 154
66 $hover->update if $hover && $hover->{can_hover}; 155 $HOVER->emit (mouse_motion => $ev)
67 $HOVER->update if $HOVER && $HOVER->{can_hover}; 156 if $HOVER;
68 }
69
70 $HOVER->mouse_motion ($ev, $HOVER->coord2local ($x, $y)) if $HOVER;
71} 157}
72 158
73# convert position array to integers 159# convert position array to integers
74sub harmonize { 160sub harmonize {
75 my ($vals) = @_; 161 my ($vals) = @_;
76 162
77 my $rem = 0; 163 my $rem = 0;
78 164
79 for ($vals) { 165 for (@$vals) {
80 my $i = int $_ + $rem; 166 my $i = int $_ + $rem;
81 $rem += $_ - $i; 167 $rem += $_ - $i;
82 $_ = $i; 168 $_ = $i;
83 } 169 }
84} 170}
85 171
172sub 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
180sub 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# call when resolution changes etc.
189sub rescale_widgets {
190 my ($sx, $sy) = @_;
191
192 for my $widget (values %WIDGET) {
193 if ($widget->{is_toplevel}) {
194 $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
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 $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
207 }
208 }
209
210 reconfigure_widgets;
211}
212
86############################################################################# 213#############################################################################
87 214
215package CFPlus::UI::Event;
216
217sub xy {
218 $_[1]->coord2local ($_[0]{x}, $_[0]{y})
219}
220
221#############################################################################
222
88package CFClient::UI::Base; 223package CFPlus::UI::Base;
89 224
90use strict; 225use strict;
91 226
92use SDL::OpenGL; 227use CFPlus::OpenGL;
93 228
94sub new { 229sub new {
95 my $class = shift; 230 my $class = shift;
96 231
97 my $self = bless { 232 my $self = bless {
98 x => 0, 233 x => "center",
99 y => 0, 234 y => "center",
100 z => 0, 235 z => 0,
101 w => -1, 236 w => undef,
102 h => -1, 237 h => undef,
238 can_events => 1,
103 @_ 239 @_
104 }, $class; 240 }, $class;
105 241
242 CFPlus::weaken ($CFPlus::UI::WIDGET{$self+0} = $self);
243
106 for (keys %$self) { 244 for (keys %$self) {
107 if (/^connect_(.*)$/) { 245 if (/^on_(.*)$/) {
108 $self->connect ($1 => delete $self->{$_}); 246 $self->connect ($1 => delete $self->{$_});
109 } 247 }
110 } 248 }
111 249
250 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
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 $self->show if $layout->{show};
260 }
261
112 $self 262 $self
113} 263}
114 264
265sub destroy {
266 my ($self) = @_;
267
268 $self->hide;
269 $self->emit ("destroy");
270 %$self = ();
271}
272
273sub TO_JSON {
274 { __widget_ref__ => $_[0]{s_id} }
275}
276
277sub show {
278 my ($self) = @_;
279
280 return if $self->{parent};
281
282 $CFPlus::UI::ROOT->add ($self);
283}
284
285sub 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 $self->emit (visibility_change => 1);
294
295 $self->realloc if !exists $self->{req_w};
296
297 $_->set_visible for $self->children;
298}
299
300sub set_invisible {
301 my ($self) = @_;
302
303 return unless $self->{visible};
304
305 $_->set_invisible for $self->children;
306
307 delete $self->{visible};
308 delete $self->{root};
309
310 undef $GRAB if $GRAB == $self;
311 undef $HOVER if $HOVER == $self;
312
313 $CFPlus::UI::TOOLTIP_WATCHER->cb->()
314 if $TOOLTIP->{owner} == $self;
315
316 $self->emit ("focus_out");
317 $self->emit (visibility_change => 0);
318}
319
320sub set_visibility {
321 my ($self, $visible) = @_;
322
323 return if $self->{visible} == $visible;
324
325 $visible ? $self->show
326 : $self->hide;
327}
328
329sub toggle_visibility {
330 my ($self) = @_;
331
332 $self->{visible}
333 ? $self->hide
334 : $self->show;
335}
336
337sub hide {
338 my ($self) = @_;
339
340 $self->set_invisible;
341
342 $self->{parent}->remove ($self)
343 if $self->{parent};
344}
345
115sub move { 346sub move_abs {
116 my ($self, $x, $y, $z) = @_; 347 my ($self, $x, $y, $z) = @_;
117 $self->{x} = $x; 348
118 $self->{y} = $y; 349 $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;
119 $self->{z} = $z if defined $z; 351 $self->{z} = $z if defined $z;
120}
121 352
122sub needs_redraw { 353 $self->update;
123 0 354}
355
356sub set_size {
357 my ($self, $w, $h) = @_;
358
359 $self->{force_w} = $w;
360 $self->{force_h} = $h;
361
362 $self->realloc;
124} 363}
125 364
126sub size_request { 365sub size_request {
127 require Carp; 366 require Carp;
128 Carp::confess "size_request is abtract"; 367 Carp::confess "size_request is abstract";
129} 368}
130 369
131sub _size_allocate { 370sub baseline_shift {
371 0
372}
373
374sub configure {
132 my ($self, $x, $y, $w, $h) = @_; 375 my ($self, $x, $y, $w, $h) = @_;
133 376
377 if ($self->{aspect}) {
378 my ($ow, $oh) = ($w, $h);
379
380 $w = List::Util::min $w, CFPlus::ceil $h * $self->{aspect};
381 $h = List::Util::min $h, CFPlus::ceil $w / $self->{aspect};
382
383 # use alignment to adjust x, y
384
385 $x += int 0.5 * ($ow - $w);
386 $y += int 0.5 * ($oh - $h);
387 }
388
389 if ($self->{x} ne $x || $self->{y} ne $y) {
134 $self->{x} = $x; 390 $self->{x} = $x;
135 $self->{y} = $y; 391 $self->{y} = $y;
392 $self->update;
393 }
136 394
137 return unless $self->{w} != $w || $self->{h} != $h; 395 if ($self->{alloc_w} != $w || $self->{alloc_h} != $h) {
396 return unless $self->{visible};
138 397
139 $self->{w} = $w; 398 $self->{alloc_w} = $w;
140 $self->{h} = $h; 399 $self->{alloc_h} = $h;
141 400
401 $self->{root}{size_alloc}{$self+0} = $self;
142 1 402 }
143} 403}
144 404
145sub size_allocate { 405sub children {
146 my ($self, $x, $y, $w, $h) = @_; 406 # nop
147
148 $self->_size_allocate ($x, $y, $w, $h);
149} 407}
150 408
151# return top left coordinates 409sub visible_children {
152sub _topleft { 410 $_[0]->children
411}
412
413sub set_max_size {
153 my ($self, $x, $y) = @_; 414 my ($self, $w, $h) = @_;
154 415
155 $self->{parent}->_topleft ($x + $self->{x}, $y + $self->{y}); 416 $self->{max_w} = int $w if defined $w;
417 $self->{max_h} = int $h if defined $h;
418
419 $self->realloc;
420}
421
422sub set_tooltip {
423 my ($self, $tooltip) = @_;
424
425 $tooltip =~ s/^\s+//;
426 $tooltip =~ s/\s+$//;
427
428 return if $self->{tooltip} eq $tooltip;
429
430 $self->{tooltip} = $tooltip;
431
432 if ($CFPlus::UI::TOOLTIP->{owner} == $self) {
433 delete $CFPlus::UI::TOOLTIP->{owner};
434 $CFPlus::UI::TOOLTIP_WATCHER->cb->();
435 }
156} 436}
157 437
158# translate global coordinates to local coordinate system 438# translate global coordinates to local coordinate system
159sub coord2local { 439sub coord2local {
160 my ($self, $x, $y) = @_; 440 my ($self, $x, $y) = @_;
161 441
162 my ($X, $Y) = $self->_topleft; 442 Carp::confess unless $self->{parent};#d#
163 ($x - $X, $y - $Y) 443
444 $self->{parent}->coord2local ($x - $self->{x}, $y - $self->{y})
164} 445}
165 446
166# translate local coordinates to global coordinate system 447# translate local coordinates to global coordinate system
167sub coord2global { 448sub coord2global {
168 my ($self, $x, $y) = @_; 449 my ($self, $x, $y) = @_;
169 450
170 my ($X, $Y) = $self->_topleft; 451 Carp::confess unless $self->{parent};#d#
171 ($x + $X, $y + $Y)
172}
173 452
453 $self->{parent}->coord2global ($x + $self->{x}, $y + $self->{y})
454}
455
174sub focus_in { 456sub invoke_focus_in {
175 my ($self) = @_; 457 my ($self) = @_;
176 458
177 return if $FOCUS == $self; 459 return if $FOCUS == $self;
178 return unless $self->{can_focus}; 460 return unless $self->{can_focus};
179 461
180 my $focus = $FOCUS; $FOCUS = $self; 462 $FOCUS = $self;
181 $focus->update if $focus;
182 $FOCUS->update;
183}
184 463
464 $self->update;
465
466 0
467}
468
185sub focus_out { 469sub invoke_focus_out {
186 my ($self) = @_; 470 my ($self) = @_;
187 471
188 return unless $FOCUS == $self; 472 return unless $FOCUS == $self;
189 473
190 my $focus = $FOCUS; undef $FOCUS; 474 undef $FOCUS;
191 $focus->update if $focus; #?
192}
193 475
476 $self->update;
477
478 $::MAPWIDGET->grab_focus #d# focus mapwidget if no other widget has focus
479 unless $FOCUS;
480
481 0
482}
483
484sub grab_focus {
485 my ($self) = @_;
486
487 $FOCUS->emit ("focus_out") if $FOCUS;
488 $self->emit ("focus_in");
489}
490
194sub mouse_motion { } 491sub invoke_mouse_motion { 0 }
195sub button_up { } 492sub invoke_button_up { 0 }
196sub key_down { } 493sub invoke_key_down { 0 }
197sub key_up { } 494sub invoke_key_up { 0 }
495sub invoke_mouse_wheel { 0 }
198 496
199sub button_down { 497sub invoke_button_down {
200 my ($self, $ev, $x, $y) = @_; 498 my ($self, $ev, $x, $y) = @_;
201 499
202 $self->focus_in; 500 $self->grab_focus;
203}
204 501
205sub w { $_[0]{w} = $_[1] if @_ > 1; $_[0]{w} } 502 0
206sub h { $_[0]{h} = $_[1] if @_ > 1; $_[0]{h} } 503}
207sub x { $_[0]{x} = $_[1] if @_ > 1; $_[0]{x} }
208sub y { $_[0]{y} = $_[1] if @_ > 1; $_[0]{y} }
209sub z { $_[0]{z} = $_[1] if @_ > 1; $_[0]{z} }
210 504
211sub draw { 505sub connect {
506 my ($self, $signal, $cb) = @_;
507
508 push @{ $self->{signal_cb}{$signal} }, $cb;
509
510 defined wantarray and CFPlus::guard {
511 @{ $self->{signal_cb}{$signal} } = grep $_ != $cb,
512 @{ $self->{signal_cb}{$signal} };
513 }
514}
515
516sub disconnect_all {
212 my ($self) = @_; 517 my ($self, $signal) = @_;
213 518
214 return unless $self->{h} && $self->{w}; 519 delete $self->{signal_cb}{$signal};
215
216 glPushMatrix;
217 glTranslate $self->{x}, $self->{y}, 0;
218 $self->_draw;
219 glPopMatrix;
220
221 if ($self == $HOVER && $self->{can_hover}) {
222 my ($x, $y) = @$self{qw(x y)};
223
224 glColor 0, 0, 1, 0.2;
225 glEnable GL_BLEND;
226 glBlendFunc GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA;
227 glBegin GL_QUADS;
228 glVertex $x , $y;
229 glVertex $x + $self->{w}, $y;
230 glVertex $x + $self->{w}, $y + $self->{h};
231 glVertex $x , $y + $self->{h};
232 glEnd;
233 glDisable GL_BLEND;
234 }
235} 520}
236 521
237sub _draw { 522my %has_coords = (
238 my ($self) = @_; 523 button_down => 1,
524 button_up => 1,
525 mouse_motion => 1,
526 mouse_wheel => 1,
527);
239 528
240 warn "no draw defined for $self\n"; 529sub emit {
530 my ($self, $signal, @args) = @_;
531
532 # I do not really like this solution, but I do not like duplication
533 # and needlessly verbose code, either.
534 my @append
535 = $has_coords{$signal}
536 ? $args[0]->xy ($self)
537 : ();
538
539 #warn +(caller(1))[3] . "emit $signal on $self (parent $self->{parent})\n";#d#
540
541 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)
241} 550}
242 551
243sub find_widget { 552sub find_widget {
244 my ($self, $x, $y) = @_; 553 my ($self, $x, $y) = @_;
554
555 return () unless $self->{can_events};
245 556
246 return $self 557 return $self
247 if $x >= $self->{x} && $x < $self->{x} + $self->{w} 558 if $x >= $self->{x} && $x < $self->{x} + $self->{w}
248 && $y >= $self->{y} && $y < $self->{y} + $self->{h}; 559 && $y >= $self->{y} && $y < $self->{y} + $self->{h};
249 560
251} 562}
252 563
253sub set_parent { 564sub set_parent {
254 my ($self, $parent) = @_; 565 my ($self, $parent) = @_;
255 566
256 Scalar::Util::weaken ($self->{parent} = $parent); 567 CFPlus::weaken ($self->{parent} = $parent);
568 $self->set_visible if $parent->{visible};
569}
570
571sub realloc {
572 my ($self) = @_;
573
574 if ($self->{visible}) {
575 return if $self->{root}{realloc}{$self+0};
576
577 $self->{root}{realloc}{$self+0} = $self;
578 $self->{root}->update;
579 } else {
580 delete $self->{req_w};
581 delete $self->{req_h};
582 }
257} 583}
258 584
259sub update { 585sub update {
260 my ($self) = @_; 586 my ($self) = @_;
261 587
262 $self->{parent}->update 588 $self->{parent}->update
263 if $self->{parent}; 589 if $self->{parent};
264} 590}
265 591
266sub connect { 592sub reconfigure {
267 my ($self, $signal, $cb) = @_; 593 my ($self) = @_;
268 594
269 push @{ $self->{cb}{$signal} }, $cb; 595 $self->realloc;
596 $self->update;
270} 597}
271 598
272sub emit { 599# using global variables seems a bit hacky, but passing through all drawing
273 my ($self, $signal, @args) = @_; 600# functions seems pointless.
601our ($draw_x, $draw_y, $draw_w, $draw_h); # screen rectangle being drawn
274 602
275 $_->($self, @args) 603sub draw {
276 for @{$self->{cb}{$signal} || []}; 604 my ($self) = @_;
277}
278 605
606 return unless $self->{h} && $self->{w};
607
608 # update screen rectangle
609 local $draw_x = $draw_x + $self->{x};
610 local $draw_y = $draw_y + $self->{y};
611
612 # 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
616 glPushMatrix;
617 glTranslate $self->{x}, $self->{y}, 0;
618
619 if ($self == $HOVER && $self->{can_hover}) {
620 glColor 1*0.2, 0.8*0.2, 0.5*0.2, 0.2;
621 glEnable GL_BLEND;
622 glBlendFunc GL_ONE, GL_ONE_MINUS_SRC_ALPHA;
623 glBegin GL_QUADS;
624 glVertex 0 , 0;
625 glVertex $self->{w}, 0;
626 glVertex $self->{w}, $self->{h};
627 glVertex 0 , $self->{h};
628 glEnd;
629 glDisable GL_BLEND;
630 }
631
632 if ($ENV{CFPLUS_DEBUG} & 1) {
633 glPushMatrix;
634 glColor 1, 1, 0, 1;
635 glTranslate 0.375, 0.375;
636 glBegin GL_LINE_LOOP;
637 glVertex 0 , 0;
638 glVertex $self->{w} - 1, 0;
639 glVertex $self->{w} - 1, $self->{h} - 1;
640 glVertex 0 , $self->{h} - 1;
641 glEnd;
642 glPopMatrix;
643 #CFPlus::UI::Label->new (w => $self->{w}, h => $self->{h}, text => $self, fontsize => 0)->_draw;
644 }
645
646 $self->_draw;
647 glPopMatrix;
648}
649
650sub _draw {
651 my ($self) = @_;
652
653 warn "no draw defined for $self\n";
654}
655
656my $cntx;#d#
279sub DESTROY { 657sub DESTROY {
280 my ($self) = @_; 658 my ($self) = @_;
281 659
282 #$self->deactivate; 660 return if CFPlus::in_destruct;
661
662 eval { $self->destroy };
663 warn "exception during widget destruction: $@" if $@ & $@ != /during global destruction/;
664
665 delete $WIDGET{$self+0};
283} 666}
284 667
285############################################################################# 668#############################################################################
286 669
287package CFClient::UI::DrawBG; 670package CFPlus::UI::DrawBG;
288 671
289our @ISA = CFClient::UI::Base::; 672our @ISA = CFPlus::UI::Base::;
290 673
291use strict; 674use strict;
292use SDL::OpenGL; 675use CFPlus::OpenGL;
293 676
294sub new { 677sub new {
295 my $class = shift; 678 my $class = shift;
296 679
297 # range [value, low, high, page] 680 # range [value, low, high, page]
298 681
299 $class->SUPER::new ( 682 $class->SUPER::new (
300 bg => [0, 0, 0, 0.2], 683 #bg => [0, 0, 0, 0.2],
301 active_bg => [1, 1, 1, 0.5], 684 #active_bg => [1, 1, 1, 0.5],
302 @_ 685 @_
303 ) 686 )
304} 687}
305 688
306sub _draw { 689sub _draw {
307 my ($self) = @_; 690 my ($self) = @_;
308 691
692 my $color = $FOCUS == $self && $self->{active_bg}
693 ? $self->{active_bg}
694 : $self->{bg};
695
696 if ($color && (@$color < 4 || $color->[3])) {
309 my ($w, $h) = @$self{qw(w h)}; 697 my ($w, $h) = @$self{qw(w h)};
310 698
311 glEnable GL_BLEND; 699 glEnable GL_BLEND;
312 glBlendFunc GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA; 700 glBlendFunc GL_ONE, GL_ONE_MINUS_SRC_ALPHA;
313 glColor @{ $FOCUS == $self ? $self->{active_bg} : $self->{bg} }; 701 glColor_premultiply @$color;
314 702
315 glBegin GL_QUADS; 703 glBegin GL_QUADS;
316 glVertex 0 , 0; 704 glVertex 0 , 0;
317 glVertex 0 , $h; 705 glVertex 0 , $h;
318 glVertex $w, $h; 706 glVertex $w, $h;
319 glVertex $w, 0; 707 glVertex $w, 0;
320 glEnd; 708 glEnd;
321 709
322 glDisable GL_BLEND; 710 glDisable GL_BLEND;
711 }
323} 712}
324 713
325############################################################################# 714#############################################################################
326 715
327package CFClient::UI::Empty; 716package CFPlus::UI::Empty;
328 717
329our @ISA = CFClient::UI::Base::; 718our @ISA = CFPlus::UI::Base::;
330
331sub size_request {
332 (0, 0)
333}
334
335sub draw { }
336
337#############################################################################
338
339package CFClient::UI::Container;
340
341our @ISA = CFClient::UI::Base::;
342 719
343sub new { 720sub new {
344 my ($class, %arg) = @_; 721 my ($class, %arg) = @_;
722 $class->SUPER::new (can_events => 0, %arg);
723}
345 724
725sub size_request {
726 my ($self) = @_;
727
728 ($self->{w} + 0, $self->{h} + 0)
729}
730
731sub draw { }
732
733#############################################################################
734
735package CFPlus::UI::Container;
736
737our @ISA = CFPlus::UI::Base::;
738
739sub new {
740 my ($class, %arg) = @_;
741
346 my $children = delete $arg{children} || []; 742 my $children = delete $arg{children};
347 743
348 my $self = $class->SUPER::new (children => [], %arg); 744 my $self = $class->SUPER::new (
745 children => [],
746 can_events => 0,
747 %arg,
748 );
749
349 $self->add ($_) for @$children; 750 $self->add (@$children)
751 if $children;
350 752
351 $self 753 $self
754}
755
756sub realloc {
757 my ($self) = @_;
758
759 $self->{force_realloc} = 1;
760 $self->{force_size_alloc} = 1;
761 $self->SUPER::realloc;
762}
763
764sub add {
765 my ($self, @widgets) = @_;
766
767 $_->set_parent ($self)
768 for @widgets;
769
770 use sort 'stable';
771
772 $self->{children} = [
773 sort { $a->{z} <=> $b->{z} }
774 @{$self->{children}}, @widgets
775 ];
776
777 $self->realloc;
778}
779
780sub children {
781 @{ $_[0]{children} }
782}
783
784sub remove {
785 my ($self, $child) = @_;
786
787 delete $child->{parent};
788 $child->hide;
789
790 $self->{children} = [ grep $_ != $child, @{ $self->{children} } ];
791
792 $self->realloc;
793}
794
795sub clear {
796 my ($self) = @_;
797
798 my $children = delete $self->{children};
799 $self->{children} = [];
800
801 for (@$children) {
802 delete $_->{parent};
803 $_->hide;
804 }
805
806 $self->realloc;
807}
808
809sub find_widget {
810 my ($self, $x, $y) = @_;
811
812 $x -= $self->{x};
813 $y -= $self->{y};
814
815 my $res;
816
817 for (reverse $self->visible_children) {
818 $res = $_->find_widget ($x, $y)
819 and return $res;
820 }
821
822 $self->SUPER::find_widget ($x + $self->{x}, $y + $self->{y})
823}
824
825sub _draw {
826 my ($self) = @_;
827
828 $_->draw for @{$self->{children}};
829}
830
831#############################################################################
832
833package CFPlus::UI::Bin;
834
835our @ISA = CFPlus::UI::Container::;
836
837sub new {
838 my ($class, %arg) = @_;
839
840 my $child = (delete $arg{child}) || new CFPlus::UI::Empty::;
841
842 $class->SUPER::new (children => [$child], %arg)
352} 843}
353 844
354sub add { 845sub add {
355 my ($self, $child) = @_; 846 my ($self, $child) = @_;
356 847
357 $child->set_parent ($self); 848 $self->SUPER::remove ($_) for @{ $self->{children} };
358 849 $self->SUPER::add ($child);
359 use sort 'stable';
360
361 $self->{children} = [
362 sort { $a->{z} <=> $b->{z} }
363 @{$self->{children}}, $child
364 ];
365
366 $self->{w} = $self->{h} = -1;
367 $self->update;
368} 850}
369 851
370sub remove { 852sub remove {
371 my ($self, $widget) = @_; 853 my ($self, $widget) = @_;
372 854
373 $self->{children} = [ grep $_ != $widget, @{ $self->{children} } ]; 855 $self->SUPER::remove ($widget);
374 856
375 $self->size_allocate (0, 0, $self->{w}, $self->{h}); 857 $self->{children} = [new CFPlus::UI::Empty]
858 unless @{$self->{children}};
859}
860
861sub child { $_[0]->{children}[0] }
862
863sub size_request {
864 $_[0]{children}[0]->size_request
865}
866
867sub invoke_size_allocate {
868 my ($self, $w, $h) = @_;
869
870 $self->{children}[0]->configure (0, 0, $w, $h);
871
872 1
873}
874
875#############################################################################
876
877# back-buffered drawing area
878
879package CFPlus::UI::Window;
880
881our @ISA = CFPlus::UI::Bin::;
882
883use CFPlus::OpenGL;
884
885sub new {
886 my ($class, %arg) = @_;
887
888 my $self = $class->SUPER::new (%arg);
889}
890
891sub update {
892 my ($self) = @_;
893
894 $ROOT->on_post_alloc ($self => sub { $self->render_child });
895 $self->SUPER::update;
896}
897
898sub invoke_size_allocate {
899 my ($self, $w, $h) = @_;
900
901 $self->update;
902
903 $self->SUPER::invoke_size_allocate ($w, $h)
904}
905
906sub _render {
907 my ($self) = @_;
908
909 $self->{children}[0]->draw;
910}
911
912sub render_child {
913 my ($self) = @_;
914
915 $self->{texture} = new_from_opengl CFPlus::Texture $self->{w}, $self->{h}, sub {
916 glClearColor 0, 0, 0, 0;
917 glClear GL_COLOR_BUFFER_BIT;
918
919 {
920 package CFPlus::UI::Base;
921
922 local ($draw_x, $draw_y, $draw_w, $draw_h) =
923 (0, 0, $self->{w}, $self->{h});
924
925 $self->_render;
926 }
927 };
928}
929
930sub _draw {
931 my ($self) = @_;
932
933 my $tex = $self->{texture}
934 or return;
935
936 glEnable GL_TEXTURE_2D;
937 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
938 glColor 0, 0, 0, 1;
939
940 $tex->draw_quad_alpha_premultiplied (0, 0);
941
942 glDisable GL_TEXTURE_2D;
943}
944
945#############################################################################
946
947package CFPlus::UI::ViewPort;
948
949use List::Util qw(min max);
950
951our @ISA = CFPlus::UI::Window::;
952
953sub new {
954 my $class = shift;
955
956 $class->SUPER::new (
957 scroll_x => 0,
958 scroll_y => 1,
959 @_,
960 )
961}
962
963sub size_request {
964 my ($self) = @_;
965
966 my ($w, $h) = @{$self->child}{qw(req_w req_h)};
967
968 $w = 10 if $self->{scroll_x};
969 $h = 10 if $self->{scroll_y};
970
971 ($w, $h)
972}
973
974sub invoke_size_allocate {
975 my ($self, $w, $h) = @_;
976
977 my $child = $self->child;
978
979 $w = $child->{req_w} if $self->{scroll_x} && $child->{req_w};
980 $h = $child->{req_h} if $self->{scroll_y} && $child->{req_h};
981
982 $self->child->configure (0, 0, $w, $h);
983 $self->update;
984
985 1
986}
987
988sub set_offset {
989 my ($self, $x, $y) = @_;
990
991 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
994 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}
1002
1003# hmm, this does not work for topleft of $self... but we should not ask for that
1004sub coord2local {
1005 my ($self, $x, $y) = @_;
1006
1007 $self->SUPER::coord2local ($x + $self->{view_x}, $y + $self->{view_y})
1008}
1009
1010sub coord2global {
1011 my ($self, $x, $y) = @_;
1012
1013 $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)
376} 1017}
377 1018
378sub find_widget { 1019sub find_widget {
379 my ($self, $x, $y) = @_; 1020 my ($self, $x, $y) = @_;
380 1021
381 $x -= $self->{x}; 1022 if ( $x >= $self->{x} && $x < $self->{x} + $self->{w}
382 $y -= $self->{y}; 1023 && $y >= $self->{y} && $y < $self->{y} + $self->{h}
383 1024 ) {
384 my $res;
385
386 for (reverse @{ $self->{children} }) {
387 $res = $_->find_widget ($x, $y)
388 and return $res;
389 }
390
391 $self->SUPER::find_widget ($x + $self->{x}, $y + $self->{y}) 1025 $self->child->find_widget ($x + $self->{view_x}, $y + $self->{view_y})
1026 } else {
1027 $self->CFPlus::UI::Base::find_widget ($x, $y)
1028 }
392} 1029}
393 1030
394sub _draw { 1031sub _render {
395 my ($self) = @_; 1032 my ($self) = @_;
396 1033
397 $_->draw for @{$self->{children}}; 1034 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
1037 CFPlus::OpenGL::glTranslate -$self->{view_x}, -$self->{view_y};
1038
1039 $self->SUPER::_render;
398} 1040}
399 1041
400############################################################################# 1042#############################################################################
401 1043
402package CFClient::UI::Bin; 1044package CFPlus::UI::ScrolledWindow;
403 1045
404our @ISA = CFClient::UI::Container::; 1046our @ISA = CFPlus::UI::Table::;
405 1047
406sub new { 1048sub new {
407 my ($class, %arg) = @_; 1049 my ($class, %arg) = @_;
408 1050
409 my $child = (delete $arg{child}) || new CFClient::UI::Empty::; 1051 my $child = delete $arg{child};
410 1052
411 $class->SUPER::new (children => [$child], %arg) 1053 my $self;
1054
1055 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 vertical => 1,
1066 range => [0, 0, 1, 0.01], # HACK fix
1067 on_changed => sub {
1068 $self->{vpos} = $_[1];
1069 $self->{vp}->set_offset ($self->{hpos}, $self->{vpos});
1070 },
1071 ;
1072
1073 $self = $class->SUPER::new (
1074 scroll_x => 0,
1075 scroll_y => 1,
1076 can_events => 1,
1077 hslider => $hslider,
1078 vslider => $vslider,
1079 col_expand => [1, 0],
1080 row_expand => [1, 0],
1081 %arg,
1082 );
1083
1084 $self->{vp} = new CFPlus::UI::ViewPort
1085 expand => 1,
1086 scroll_x => $self->{scroll_x},
1087 scroll_y => $self->{scroll_y},
1088 on_changed => sub {
1089 my ($vp, $x, $y) = @_;
1090
1091 $vp->{parent}{hslider}->set_value ($x);
1092 $vp->{parent}{vslider}->set_value ($y);
1093
1094 0
1095 },
1096 ;
1097
1098 $self->SUPER::add_at (0, 0, $self->{vp});
1099
1100 $self->add ($child) if $child;
1101
1102 $self
412} 1103}
1104
1105#TODO# update range on size_allocate depending on child
413 1106
414sub add { 1107sub add {
415 my ($self, $widget) = @_; 1108 my ($self, $widget) = @_;
416 1109
417 $self->{children} = []; 1110 $self->{vp}->add ($self->{child} = $widget);
1111}
1112
1113sub update_slider {
1114 my ($self) = @_;
1115
1116 my $child = ($self->{vp} or return)->child;
1117
1118 my ($w1, $w2) = ($child->{w}, $self->{vp}{w});
1119 $self->{hslider}->set_range ([$self->{hslider}{range}[0], 0, $w1, $w2, 1]);
1120
1121 my $visible = $w1 > $w2;
1122 if ($visible != $self->{hslider}{visible}) {
1123 $visible ? $self->SUPER::add_at (0, 1, $self->{hslider})
1124 : $self->{hslider}->hide;
1125 }
1126
1127 my ($h1, $h2) = ($child->{h}, $self->{vp}{h});
1128 $self->{vslider}->set_range ([$self->{vslider}{range}[0], 0, $h1, $h2, 1]);
1129
1130 my $visible = $h1 > $h2;
1131 if ($visible != $self->{vslider}{visible}) {
1132 $visible ? $self->SUPER::add_at (1, 0, $self->{vslider})
1133#!/opt/bin/perl
1134
1135my $startup_done = sub { };
1136our $PANGO = "1.5.0";
1137
1138# do splash-screen thingy on win32
1139BEGIN {
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
1157use strict;
1158use utf8;
1159
1160use Carp 'verbose';
1161
1162# do things only needed for single-binary version (par)
1163BEGIN {
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
1203unshift @INC, $ENV{PAR_TEMP}
1204 if %PAR::LibCache;
1205
1206use Time::HiRes 'time';
1207use Event;
1208
1209use Crossfire;
1210use Crossfire::Protocol::Constants;
1211
1212use Compress::LZF;
1213
1214use CFPlus;
1215use CFPlus::OpenGL ();
1216use CFPlus::Protocol;
1217use CFPlus::DB;
1218use CFPlus::UI;
1219use CFPlus::UI::Inventory;
1220use CFPlus::UI::SpellList;
1221use CFPlus::Pod;
1222use CFPlus::MapWidget;
1223use 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
1233my $MAX_FPS = 60;
1234my $MIN_FPS = 5; # unused as of yet
1235
1236our $META_SERVER = "http://metaserver.schmorp.de/current.json";
1237
1238our $LAST_REFRESH;
1239our $NOW;
1240
1241our $CFG;
1242our $CONN;
1243our $PROFILE; # current profile
1244our $FAST; # fast, low-quality mode, possibly useful for software-rendering
1245
1246our $WANT_REFRESH;
1247our $CAN_REFRESH;
1248
1249our @SDL_MODES;
1250our $WIDTH;
1251our $HEIGHT;
1252our $FULLSCREEN;
1253our $FONTSIZE;
1254
1255our $FONT_PROP;
1256our $FONT_FIXED;
1257
1258our $MAP;
1259our $MAPMAP;
1260our $MAPWIDGET;
1261our $BUTTONBAR;
1262our $LOGVIEW;
1263our $CONSOLE;
1264our $METASERVER;
1265our $LOGIN_BUTTON;
1266our $QUIT_DIALOG;
1267our $HOST_ENTRY;
1268our $FULLSCREEN_ENABLE;
1269our $PICKUP_ENABLE;
1270our $SERVER_INFO;
1271
1272our $SETUP_DIALOG;
1273our $SETUP_NOTEBOOK;
1274our $SETUP_SERVER;
1275our $SETUP_KEYBOARD;
1276
1277our $PL_NOTEBOOK;
1278our $PL_WINDOW;
1279
1280our $INVENTORY_PAGE;
1281our $STATS_PAGE;
1282our $SKILL_PAGE;
1283our $SPELL_PAGE;
1284our $SPELL_LIST;
1285
1286our $HELP_WINDOW;
1287our $MESSAGE_WINDOW;
1288our $FLOORBOX;
1289our $GAUGES;
1290our $STATWIDS;
1291
1292our $SDL_ACTIVE;
1293our %SDL_CB;
1294
1295our $SDL_MIXER;
1296our $MUSIC_DEFAULT = "in_a_heartbeat.ogg";
1297our @MUSIC_WANT;
1298our $MUSIC_START;
1299our $MUSIC_PLAYING;
1300our $MUSIC_PLAYER;
1301our $MUSIC_RESUME = 30; # resume music when players less than these many seconds before
1302our @SOUNDS; # event => file mapping
1303our %AUDIO_CHUNKS; # audio files
1304
1305our $ALT_ENTER_MESSAGE;
1306our $STATUSBOX;
1307our $DEBUG_STATUS;
1308
1309our $INV;
1310our $INVR;
1311our $INV_RIGHT_HB;
1312
1313our $PICKUP_CFG;
1314
1315our $IN_BUILD_MODE;
1316our $BUILD_BUTTON;
1317
1318sub status {
1319 $STATUSBOX->add (CFPlus::asxml $_[0], pri => -10, group => "status", timeout => 10, fg => [1, 1, 0, 1]);
1320}
1321
1322sub debug {
1323 $DEBUG_STATUS->set_text ($_[0]);
1324}
1325
1326sub 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
1337sub 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#
1343our $QUERY_TIMER; #d#
1344
1345# server query dialog
1346sub 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
1573sub 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;
418 1584
419 $self->SUPER::add ($widget); 1585 $CONN = eval {
420} 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,
421 1593
422sub remove { 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
1630sub 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
1651sub 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
1778sub 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
1821sub set_gauge_window_fontsize {
1822 for (map { $GAUGES->{$_} } grep { $_ ne 'win' } keys %{$GAUGES}) {
1823 $_->set_fontsize ($::CFG->{gauge_fontsize});
1824 }
1825}
1826
1827sub 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
1874sub 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
1892sub 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
2041sub 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
2047sub formsep($) {
2048 scalar reverse join ",", unpack "(A3)*", reverse $_[0] * 1
2049}
2050
2051my $METASERVER_ATIME;
2052
2053sub 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
2149sub 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
2174sub 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
2289sub 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
2334sub 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
2397sub 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
2502my %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
2521sub 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
2564sub toggle_player_page {
423 my ($self, $widget) = @_; 2565 my ($widget) = @_;
424 2566
425 $self->SUPER::remove ($widget); 2567 if ($PL_WINDOW->{visible} && $PL_NOTEBOOK->get_current_page == $widget) {
426 2568 $PL_WINDOW->hide;
427 $self->{children} = [new CFClient::UI::Empty] 2569 } else {
428 unless @{$self->{children}}; 2570 $PL_NOTEBOOK->set_current_page ($widget);
2571 $PL_WINDOW->show;
2572 }
429} 2573}
430 2574
431sub child { $_[0]->{children}[0] } 2575sub 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 ;
432 2585
433sub size_request { 2586 my $ntb =
434 $_[0]{children}[0]->size_request 2587 $PL_NOTEBOOK =
435} 2588 new CFPlus::UI::Notebook expand => 1;
436 2589
437sub size_allocate { 2590 $ntb->add (
438 my ($self, $x, $y, $w, $h) = @_; 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 );
439 2598
440 $self->_size_allocate ($x, $y, $w, $h) or return; 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.");
441 2612
442 $self->{children}[0]->size_allocate (0, 0, $w, $h); 2613 $ntb->set_current_page ($INVENTORY_PAGE);
2614
2615 $plwin->add ($ntb);
2616 $plwin
443} 2617}
2618
2619sub keyboard_setup {
2620 CFPlus::Macro::keyboard_setup
2621}
2622
2623sub 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
2725sub 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
2746sub 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
2783sub 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
2837sub sdl_init {
2838 CFPlus::SDL_Init
2839 and die "SDL::Init failed!\n";
2840}
2841
2842sub 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
3008sub 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
3029sub 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
3083sub 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
3127sub video_shutdown {
3128 CFPlus::OpenGL::shutdown;
3129
3130 undef $SDL_ACTIVE;
3131}
3132
3133sub audio_channel_finished {
3134 my ($channel) = @_;
3135
3136 #warn "channel $channel finished\n";#d#
3137}
3138
3139sub 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
3153sub 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
3180sub 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
3211sub audio_music_finished {
3212 $MUSIC_PLAYING = undef;
3213 undef $MUSIC_PLAYER;
3214
3215 audio_music_changed;
3216}
3217
3218sub 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
3254sub audio_shutdown {
3255 CFPlus::Mix_CloseAudio if $SDL_MIXER;
3256 undef $SDL_MIXER;
3257 @SOUNDS = ();
3258 %AUDIO_CHUNKS = ();
3259}
3260
3261my %animate_object;
3262my $animate_timer;
3263
3264my $fps = 9;
3265
3266my %demo;#d#
3267
3268sub 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
3281my $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
3299sub animation_start {
3300 my ($widget) = @_;
3301 $animate_object{$widget} = $widget;
3302}
3303
3304sub 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
3313Event->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);
444 3357
445############################################################################# 3358#############################################################################
446 3359
447package CFClient::UI::Window; 3360$SIG{INT} = $SIG{TERM} = sub { exit };
448 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
3469show_tip_of_the_day if $CFG->{show_tips};
3470
3471Event::loop;
3472#CFPlus::SDL_Quit;
3473#CFPlus::_exit 0;
3474
3475END {
3476 CFPlus::SDL_Quit;
3477 CFPlus::DB::Server::stop;
3478}
3479
3480=head1 NAME
3481
3482cfplus - A Crossfire+ and Crossfire game client
3483
3484=head1 SYNOPSIS
3485
3486Just run it - no commandline arguments are supported.
3487
3488=head1 USAGE
3489
3490cfplus utilises OpenGL for all UI elements and the game. It is supposed to be used
3491fullscreen and interactively.
3492
3493=head1 DEBUGGING
3494
3495
3496CFPLUS_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
3505Marc Lehmann <crossfire@schmorp.de>, Robin Redeker <elmex@ta-sa.org>
3506
3507
3508
3509 : $self->{vslider}->hide;
3510 }
3511}
3512
3513sub update {
3514 my ($self) = @_;
3515
3516 $self->SUPER::update;
3517 $self->update_slider;
3518}
3519
3520sub 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
3530sub invoke_button_down {
3531 my ($self, $ev, $x, $y) = @_;
3532
3533 if ($ev->{button} == 2) {
3534 $self->grab_focus;
3535
3536 my $ox = $self->{vp}{view_x} + $ev->{x};
3537 my $oy = $self->{vp}{view_y} + $ev->{y};
3538
3539 $self->{motion} = sub {
3540 my ($ev, $x, $y) = @_;
3541
3542 $self->{vp}->set_offset ($ox - $ev->{x}, $oy - $ev->{y});
3543 $self->update;
3544 };
3545
3546 return 1;
3547 }
3548
3549 0
3550}
3551
3552sub invoke_button_up {
3553 my ($self, $ev, $x, $y) = @_;
3554
3555 if (delete $self->{motion}) {
3556 return 1;
3557 }
3558
3559 0
3560}
3561
3562sub 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
3573sub invoke_size_allocate {
3574 my ($self, $w, $h) = @_;
3575
3576 $self->update_slider;
3577 $self->SUPER::invoke_size_allocate ($w, $h)
3578}
3579
3580#############################################################################
3581
3582package CFPlus::UI::Frame;
3583
449our @ISA = CFClient::UI::Bin::; 3584our @ISA = CFPlus::UI::Bin::;
450 3585
451use SDL::OpenGL; 3586use CFPlus::OpenGL;
3587
3588sub new {
3589 my $class = shift;
3590
3591 $class->SUPER::new (
3592 bg => undef,
3593 @_,
3594 )
3595}
3596
3597sub _draw {
3598 my ($self) = @_;
3599
3600 if ($self->{bg}) {
3601 my ($w, $h) = @$self{qw(w h)};
3602
3603 glEnable GL_BLEND;
3604 glBlendFunc GL_ONE, GL_ONE_MINUS_SRC_ALPHA;
3605 glColor_premultiply @{ $self->{bg} };
3606
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#############################################################################
3621
3622package CFPlus::UI::FancyFrame;
3623
3624our @ISA = CFPlus::UI::Bin::;
3625
3626use CFPlus::OpenGL;
452 3627
453sub new { 3628sub new {
454 my ($class, %arg) = @_; 3629 my ($class, %arg) = @_;
455 3630
3631 if ((exists $arg{label}) && !ref $arg{label}) {
3632 $arg{label} = new CFPlus::UI::Label
3633 align => 1,
3634 valign => 0,
3635 text => $arg{label},
3636 fontsize => ($arg{border} || 0.8) * 0.75;
3637 }
3638
456 my $self = $class->SUPER::new (%arg); 3639 my $self = $class->SUPER::new (
457} 3640 # label => "",
3641 fg => [0.6, 0.3, 0.1],
3642 border => 0.8,
3643 style => 'single',
3644 %arg,
3645 );
458 3646
459sub update { 3647 $self
3648}
3649
3650sub add {
3651 my ($self, @widgets) = @_;
3652
3653 $self->SUPER::add (@widgets);
3654 $self->CFPlus::UI::Container::add ($self->{label}) if $self->{label};
3655}
3656
3657sub border {
3658 int $_[0]{border} * $::FONTSIZE
3659}
3660
3661sub 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
3675sub invoke_size_allocate {
460 my ($self) = @_; 3676 my ($self, $w, $h) = @_;
461 3677
462 # we want to do this delayed... 3678 my $border = $self->border;
463 $self->render_chld;
464 $self->SUPER::update;
465}
466 3679
467sub render_chld { 3680 $w -= List::Util::max 0, $border * 2;
468 my ($self) = @_; 3681 $h -= List::Util::max 0, $border * 2;
469 3682
470 $self->{texture} = new_from_opengl CFClient::Texture $self->{w}, $self->{h}, sub { 3683 if (my $label = $self->{label}) {
471 glClearColor 0, 0, 0, 1; 3684 $label->{w} = List::Util::max 0, List::Util::min $self->{label_w}, $w - $border * 2;
472 glClear GL_COLOR_BUFFER_BIT; 3685 $label->{h} = List::Util::min $h, $border;
473 $self->child->draw; 3686 $label->invoke_size_allocate ($label->{w}, $label->{h});
474 }; 3687 }
475}
476 3688
477sub size_allocate { 3689 $self->child->configure ($border, $border, $w, $h);
478 my ($self, $x, $y, $w, $h) = @_;
479 3690
480 $self->_size_allocate ($x, $y, $w, $h) or return; 3691 1
481
482 $self->child->size_allocate (0, 0, $w, $h);
483
484 $self->render_chld;
485} 3692}
486 3693
487sub _draw { 3694sub _draw {
488 my ($self) = @_; 3695 my ($self) = @_;
489 3696
3697 my $child = $self->{children}[0];
3698
3699 my $border = $self->border;
490 my ($w, $h) = ($self->w, $self->h); 3700 my ($w, $h) = ($self->{w}, $self->{h});
491 3701
492 my $tex = $self->{texture} 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
3722package CFPlus::UI::Toplevel;
3723
3724our @ISA = CFPlus::UI::Bin::;
3725
3726use CFPlus::OpenGL;
3727
3728my $bg =
3729 new_from_file CFPlus::Texture CFPlus::find_rcfile "d1_bg.png",
3730 mipmap => 1, wrap => 1;
3731
3732my @border =
3733 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
3734 qw(d1_border_top.png d1_border_right.png d1_border_left.png d1_border_bottom.png);
3735
3736my @icon =
3737 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
3738 qw(x1_move.png x1_resize.png);
3739
3740sub new {
3741 my ($class, %arg) = @_;
3742
3743 my $self = $class->SUPER::new (
3744 bg => [1, 1, 1, 1],
3745 border_bg => [1, 1, 1, 1],
3746 border => 0.6,
3747 can_events => 1,
3748 min_w => 64,
3749 min_h => 32,
3750 %arg,
3751 );
3752
3753 $self->{title_widget} = new CFPlus::UI::Label
3754 align => 0,
3755 valign => 1,
3756 text => $self->{title},
3757 fontsize => $self->{border},
3758 if exists $self->{title};
3759
3760 if ($self->{has_close_button}) {
3761 $self->{close_button} =
3762 new CFPlus::UI::ImageButton
3763 path => 'x1_close.png',
3764 on_activate => sub { $self->emit ("delete") };
3765
3766 $self->CFPlus::UI::Container::add ($self->{close_button});
3767 }
3768
3769 $self
3770}
3771
3772sub add {
3773 my ($self, @widgets) = @_;
3774
3775 $self->SUPER::add (@widgets);
3776 $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}
3779
3780sub border {
3781 int $_[0]{border} * $::FONTSIZE
3782}
3783
3784sub size_request {
3785 my ($self) = @_;
3786
3787 $self->{title_widget}->size_request
3788 if $self->{title_widget};
3789
3790 $self->{close_button}->size_request
3791 if $self->{close_button};
3792
3793 my ($w, $h) = $self->SUPER::size_request;
3794
3795 (
3796 $w + $self->border * 2,
3797 $h + $self->border * 2,
3798 )
3799}
3800
3801sub invoke_size_allocate {
3802 my ($self, $w, $h) = @_;
3803
3804 if ($self->{title_widget}) {
3805 $self->{title_widget}{w} = $w;
3806 $self->{title_widget}{h} = $h;
3807 $self->{title_widget}->invoke_size_allocate ($w, $h);
3808 }
3809
3810 my $border = $self->border;
3811
3812 $h -= List::Util::max 0, $border * 2;
3813 $w -= List::Util::max 0, $border * 2;
3814
3815 $self->child->configure ($border, $border, $w, $h);
3816
3817 $self->{close_button}->configure ($self->{w} - $border, 0, $border, $border)
3818 if $self->{close_button};
3819
3820 1
3821}
3822
3823sub invoke_delete {
3824 my ($self) = @_;
3825
3826 $self->hide;
3827
3828 1
3829}
3830
3831sub invoke_button_down {
3832 my ($self, $ev, $x, $y) = @_;
3833
3834 my ($w, $h) = @$self{qw(w h)};
3835 my $border = $self->border;
3836
3837 my $lr = ($x >= 0 && $x < $border) || ($x > $w - $border && $x < $w);
3838 my $td = ($y >= 0 && $y < $border) || ($y > $h - $border && $y < $h);
3839
3840 if ($lr & $td) {
3841 my ($wx, $wy) = ($self->{x}, $self->{y});
3842 my ($ox, $oy) = ($ev->{x}, $ev->{y});
3843 my ($bw, $bh) = ($self->{w}, $self->{h});
3844
3845 my $mx = $x < $border;
3846 my $my = $y < $border;
3847
3848 $self->{motion} = sub {
3849 my ($ev, $x, $y) = @_;
3850
3851 my $dx = $ev->{x} - $ox;
3852 my $dy = $ev->{y} - $oy;
3853
3854 $self->{force_w} = $bw + $dx * ($mx ? -1 : 1);
3855 $self->{force_h} = $bh + $dy * ($my ? -1 : 1);
3856
3857 $self->move_abs ($wx + $dx * $mx, $wy + $dy * $my);
3858 $self->realloc;
3859 };
3860
3861 } elsif ($lr ^ $td) {
3862 my ($ox, $oy) = ($ev->{x}, $ev->{y});
3863 my ($bx, $by) = ($self->{x}, $self->{y});
3864
3865 $self->{motion} = sub {
3866 my ($ev, $x, $y) = @_;
3867
3868 ($x, $y) = ($ev->{x}, $ev->{y});
3869
3870 $self->move_abs ($bx + $x - $ox, $by + $y - $oy);
3871 # HACK: the next line is required to enforce placement
3872 $self->{parent}->invoke_size_allocate ($self->{parent}{w}, $self->{parent}{h});
3873 };
3874 } else {
493 or return; 3875 return 0;
3876 }
494 3877
495 glEnable GL_BLEND; 3878 1
496 glBlendFunc GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA; 3879}
3880
3881sub invoke_button_up {
3882 my ($self, $ev, $x, $y) = @_;
3883
3884 ! ! delete $self->{motion}
3885}
3886
3887sub invoke_mouse_motion {
3888 my ($self, $ev, $x, $y) = @_;
3889
3890 $self->{motion}->($ev, $x, $y) if $self->{motion};
3891
3892 ! ! $self->{motion}
3893}
3894
3895sub invoke_visibility_change {
3896 my ($self, $visible) = @_;
3897
3898 delete $self->{motion} unless $visible;
3899
3900 0
3901}
3902
3903sub _draw {
3904 my ($self) = @_;
3905
3906 my $child = $self->{children}[0];
3907
3908 my ($w, $h ) = ($self->{w}, $self->{h});
3909 my ($cw, $ch) = ($child->{w}, $child->{h});
3910
497 glEnable GL_TEXTURE_2D; 3911 glEnable GL_TEXTURE_2D;
498 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE; 3912 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE;
499 3913
500 $tex->draw_quad (0, 0, $w, $h); 3914 my $border = $self->border;
501 3915
502 glDisable GL_BLEND; 3916 glColor @{ $self->{border_bg} };
3917 $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
3936 if (@{$self->{bg}} < 4 || $self->{bg}[3]) {
3937 glColor @{ $self->{bg} };
3938
3939 # TODO: repeat texture not scale
3940 # solve this better(?)
3941 $bg->{s} = $cw / $bg->{w};
3942 $bg->{t} = $ch / $bg->{h};
3943 $bg->draw_quad_alpha ($border, $border, $cw, $ch);
3944 }
3945
503 glDisable GL_TEXTURE_2D; 3946 glDisable GL_TEXTURE_2D;
3947
3948 $child->draw;
3949
3950 if ($self->{title_widget}) {
3951 glTranslate 0, $border - $self->{h};
3952 $self->{title_widget}->_draw;
3953
3954 glTranslate 0, - ($border - $self->{h});
3955 }
3956
3957 $self->{close_button}->draw
3958 if $self->{close_button};
504} 3959}
505 3960
506############################################################################# 3961#############################################################################
507 3962
508package CFClient::UI::Frame; 3963package CFPlus::UI::Table;
509 3964
510our @ISA = CFClient::UI::Bin::; 3965our @ISA = CFPlus::UI::Base::;
511 3966
512use SDL::OpenGL; 3967use List::Util qw(max sum);
513 3968
514sub size_request { 3969use CFPlus::OpenGL;
515 my ($self) = @_;
516 my $chld = $self->child
517 or return (0, 0);
518
519 $chld->move (2, 2);
520
521 map { $_ + 4 } $chld->size_request;
522}
523
524sub size_allocate {
525 my ($self, $x, $y, $w, $h) = @_;
526
527 $self->_size_allocate ($x, $y, $w, $h) or return;
528
529 $self->child->size_allocate (2, 2, $w - 4, $h - 4);
530}
531
532sub _draw {
533 my ($self) = @_;
534
535 my $chld = $self->child;
536
537 my ($w, $h) = $chld->size_request;
538
539 glBegin GL_QUADS;
540 glColor 0, 0, 0;
541 glVertex 0 , 0;
542 glVertex 0 , $h + 4;
543 glVertex $w + 4 , $h + 4;
544 glVertex $w + 4 , 0;
545 glEnd;
546
547 $chld->draw;
548}
549
550#############################################################################
551
552package CFClient::UI::FancyFrame;
553
554our @ISA = CFClient::UI::Bin::;
555
556use SDL::OpenGL;
557
558my @tex =
559 map { new_from_file CFClient::Texture CFClient::find_rcfile $_ }
560 qw(d1_bg.png d1_border_top.png d1_border_right.png d1_border_left.png d1_border_bottom.png);
561 3970
562sub new { 3971sub new {
563 my $class = shift; 3972 my $class = shift;
564 3973
565 # TODO: user_x, user_y, overwrite moveto?
566
567 $class->SUPER::new ( 3974 $class->SUPER::new (
568 bg => [1, 1, 1, 1], 3975 children => [],
569 border_bg => [1, 1, 1, 1], 3976 col_expand => [],
570 border => $::FONTSIZE * 0.8, 3977 row_expand => [],
571 @_ 3978 @_,
572 ) 3979 )
573} 3980}
574 3981
575sub size_request { 3982sub children {
576 my ($self) = @_; 3983 grep $_, map @$_, grep $_, @{ $_[0]{children} }
577
578 return ($self->{user_w}, $self->{user_h}) if $self->{user_w} && $self->{user_h};
579
580 my ($w, $h) = $self->SUPER::size_request;
581
582 (
583 $w + $self->{border} * 2,
584 $h + $self->{border} * 2,
585 )
586} 3984}
587 3985
588sub size_allocate { 3986# TODO: store row/col info in child widget and use standard add/del
589 my ($self, $x, $y, $w, $h) = @_;
590
591 $self->_size_allocate ($x, $y, $w, $h) or return;
592
593 $h -= List::Util::max 0, $self->{border} * 2;
594 $w -= List::Util::max 0, $self->{border} * 2;
595
596 $self->child->size_allocate ($self->{border}, $self->{border}, $w, $h);
597}
598
599sub button_down {
600 my ($self, $ev, $x, $y) = @_;
601
602 if ($x < $self->{w} && $x >= $self->{w} - $self->{border}
603 && $y < $self->{h} && $y >= $self->{h} - $self->{border}) {
604
605 my ($ox, $oy) = ($ev->button_x, $ev->button_y);
606 my ($bw, $bh) = ($self->{w}, $self->{h});
607
608 $self->{motion} = sub {
609 my ($ev, $x, $y) = @_;
610
611 ($x, $y) = ($ev->motion_x, $ev->motion_y);
612
613 $self->{user_w} = $bw + $x - $ox;
614 $self->{user_h} = $bh + $y - $oy;
615 $self->update;
616 };
617
618 } elsif ($x >= 0 && $x < $self->{w}
619 && $y >= 0 && $y < $self->{border}) {
620
621 my ($ox, $oy) = ($ev->button_x, $ev->button_y);
622 my ($bx, $by) = ($self->{x}, $self->{y});
623
624 $self->{motion} = sub {
625 my ($ev, $x, $y) = @_;
626
627 ($x, $y) = ($ev->motion_x, $ev->motion_y);
628
629 $self->move ($bx + $x - $ox, $by + $y - $oy);
630 $self->update;
631 };
632 }
633}
634
635sub button_up {
636 my ($self, $ev, $x, $y) = @_;
637
638 delete $self->{motion};
639}
640
641sub mouse_motion {
642 my ($self, $ev, $x, $y) = @_;
643
644 $self->{motion}->($ev, $x, $y) if $self->{motion};
645}
646
647sub _draw {
648 my ($self) = @_;
649
650 my ($w, $h ) = ($self->{w}, $self->{h});
651 my ($cw, $ch) = ($self->child->{w}, $self->child->{h});
652
653 glEnable GL_BLEND;
654 glBlendFunc GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA;
655 glEnable GL_TEXTURE_2D;
656 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE;
657
658 glColor @{ $self->{border_bg} };
659 $tex[1]->draw_quad (0, 0, $w, $self->{border});
660 $tex[3]->draw_quad (0, $self->{border}, $self->{border}, $ch);
661 $tex[2]->draw_quad ($w - $self->{border}, $self->{border}, $self->{border}, $ch);
662 $tex[4]->draw_quad (0, $h - $self->{border}, $w, $self->{border});
663
664 my $bg = $tex[0];
665
666 # TODO: repeat texture not scale
667 my $rep_x = $cw / $bg->{w};
668 my $rep_y = $ch / $bg->{h};
669
670 glColor @{ $self->{bg} };
671
672 $bg->{s} = $rep_x;
673 $bg->{t} = $rep_y;
674 $bg->{wrap_mode} = 1;
675 $bg->draw_quad ($self->{border}, $self->{border}, $cw, $ch);
676
677 glDisable GL_TEXTURE_2D;
678 glDisable GL_BLEND;
679
680 $self->child->draw;
681}
682
683#############################################################################
684
685package CFClient::UI::Table;
686
687our @ISA = CFClient::UI::Base::;
688
689use List::Util qw(max sum);
690
691use SDL::OpenGL;
692
693sub new {
694 my $class = shift;
695
696 $class->SUPER::new (
697 col_expand => [],
698 @_
699 )
700}
701
702sub add { 3987sub add {
703 my ($self, $x, $y, $child) = @_; 3988 my $self = shift;
704 3989
3990 Carp::cluck "please use the add_at method instead of calling add, thank you.\n";#d#
3991 $self->add_at (@_);
3992}
3993
3994sub add_at {
3995 my $self = shift;
3996
3997 while (@_) {
3998 my ($col, $row, $child) = splice @_, 0, 3, ();
3999
705 $child->set_parent ($self); 4000 $child->set_parent ($self);
706 $self->{children}[$y][$x] = $child; 4001 $self->{children}[$row][$col] = $child;
4002 }
707 4003
708 $self->{w} = $self->{h} = -1; 4004 $self->{force_realloc} = 1;
709 $self->update; 4005 $self->{force_size_alloc} = 1;
4006 $self->realloc;
710} 4007}
711 4008
4009sub remove {
4010 my ($self, $child) = @_;
4011
4012 for (@{ $self->{children} }) {
4013 for (@{ $_ || [] }) {
4014 $_ = undef if $_ == $child;
4015 }
4016 }
4017}
4018
712# TODO: move to container class maybe? send childs a signal on removal? 4019# TODO: move to container class maybe? send children a signal on removal?
713sub clear { 4020sub clear {
714 my ($self) = @_; 4021 my ($self) = @_;
715 4022
4023 my @children = $self->children;
716 delete $self->{children}; 4024 delete $self->{children};
717 $self->update; 4025
4026 for (@children) {
4027 delete $_->{parent};
4028 $_->hide;
4029 }
4030
4031 $self->realloc;
718} 4032}
719 4033
720sub get_wh { 4034sub get_wh {
721 my ($self) = @_; 4035 my ($self) = @_;
722 4036
727 or next; 4041 or next;
728 4042
729 for my $x (0 .. $#$row) { 4043 for my $x (0 .. $#$row) {
730 my $widget = $row->[$x] 4044 my $widget = $row->[$x]
731 or next; 4045 or next;
732 my ($w, $h) = $widget->size_request; 4046 my ($w, $h) = @$widget{qw(req_w req_h)};
733 4047
734 $w[$x] = max $w[$x], $w; 4048 $w[$x] = max $w[$x], $w;
735 $h[$y] = max $h[$y], $h; 4049 $h[$y] = max $h[$y], $h;
736 } 4050 }
737 } 4051 }
748 (sum @$ws), 4062 (sum @$ws),
749 (sum @$hs), 4063 (sum @$hs),
750 ) 4064 )
751} 4065}
752 4066
753sub size_allocate { 4067sub invoke_size_allocate {
754 my ($self, $x, $y, $w, $h) = @_; 4068 my ($self, $w, $h) = @_;
755
756 $self->_size_allocate ($x, $y, $w, $h) or return;
757 4069
758 my ($ws, $hs) = $self->get_wh; 4070 my ($ws, $hs) = $self->get_wh;
759 4071
760 my $req_w = sum @$ws; 4072 my $req_w = (sum @$ws) || 1;
761 my $req_h = sum @$hs; 4073 my $req_h = (sum @$hs) || 1;
762 4074
763 # TODO: nicer code && do row_expand 4075 # TODO: nicer code
764 my @col_expand = @{$self->{col_expand}}; 4076 my @col_expand = @{$self->{col_expand}};
765 @col_expand = (1) x @$ws unless @col_expand; 4077 @col_expand = (1) x @$ws unless @col_expand;
766 my $col_expand = (sum @col_expand) || 1; 4078 my $col_expand = (sum @col_expand) || 1;
767 4079
768 # linearly scale sizes
769 $ws->[$_] += $col_expand[$_] / $col_expand * ($w - $req_w) for 0 .. $#$ws; 4080 $ws->[$_] += $col_expand[$_] / $col_expand * ($w - $req_w) for 0 .. $#$ws;
770 $hs->[$_] *= 1 * $h / $req_h for 0 .. $#$hs;
771 4081
772 CFClient::UI::harmonize $ws; 4082 CFPlus::UI::harmonize $ws;
4083
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
773 CFClient::UI::harmonize $hs; 4090 CFPlus::UI::harmonize $hs;
774 4091
775 my $y; 4092 my $y;
776 4093
777 for my $r (0 .. $#{$self->{children}}) { 4094 for my $r (0 .. $#{$self->{children}}) {
778 my $row = $self->{children}[$r] 4095 my $row = $self->{children}[$r]
783 4100
784 for my $c (0 .. $#$row) { 4101 for my $c (0 .. $#$row) {
785 my $col_w = $ws->[$c]; 4102 my $col_w = $ws->[$c];
786 4103
787 if (my $widget = $row->[$c]) { 4104 if (my $widget = $row->[$c]) {
788 $widget->size_allocate ($x, $y, $col_w, $row_h); 4105 $widget->configure ($x, $y, $col_w, $row_h);
789 } 4106 }
790 4107
791 $x += $col_w; 4108 $x += $col_w;
792 } 4109 }
793 4110
794 $y += $row_h; 4111 $y += $row_h;
795 } 4112 }
796 4113
4114 1
797} 4115}
798 4116
799sub find_widget { 4117sub find_widget {
800 my ($self, $x, $y) = @_; 4118 my ($self, $x, $y) = @_;
801 4119
820 } 4138 }
821} 4139}
822 4140
823############################################################################# 4141#############################################################################
824 4142
825package CFClient::UI::HBox; 4143package CFPlus::UI::Fixed;
826 4144
827# TODO: wrap into common Box base class 4145use List::Util qw(min max);
828 4146
829our @ISA = CFClient::UI::Container::; 4147our @ISA = CFPlus::UI::Container::;
4148
4149sub 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
4156sub _scale($$$) {
4157 my ($mode, $val, $max) = @_;
4158
4159 $mode eq "abs" ? $val
4160 : $mode eq "rel" ? $val * $max
4161 : 0
4162}
830 4163
831sub size_request { 4164sub size_request {
832 my ($self) = @_; 4165 my ($self) = @_;
833 4166
834 my @alloc = map [$_->size_request], @{$self->{children}}; 4167 my ($x1, $y1, $x2, $y2) = (0, 0, 0, 0);
835 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 }
836 ( 4180 }
837 (List::Util::sum map $_->[0], @alloc), 4181
838 (List::Util::max map $_->[1], @alloc), 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
4206sub 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
4225package CFPlus::UI::Box;
4226
4227our @ISA = CFPlus::UI::Container::;
4228
4229sub size_request {
4230 my ($self) = @_;
4231
4232 $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}
4242
4243sub invoke_size_allocate {
4244 my ($self, $w, $h) = @_;
4245
4246 my $space = $self->{vertical} ? $h : $w;
4247 my @children = $self->visible_children;
4248
4249 my @req;
4250
4251 if ($self->{homogeneous}) {
4252 @req = ($space / (@children || 1)) x @children;
4253 } else {
4254 @req = map $_->{$self->{vertical} ? "req_h" : "req_w"}, @children;
4255 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 my $expand = (List::Util::sum map $_->{expand}, @children) || 1;
4262
4263 $space = ($space - $req) / $expand; # remaining space to give away
4264
4265 $req[$_] += $space * $children[$_]{expand}
4266 for 0 .. $#children;
4267 }
4268 }
4269
4270 CFPlus::UI::harmonize \@req;
4271
4272 my $pos = 0;
4273 for (0 .. $#children) {
4274 my $alloc = $req[$_];
4275 $children[$_]->configure ($self->{vertical} ? (0, $pos, $w, $alloc) : ($pos, 0, $alloc, $h));
4276
4277 $pos += $alloc;
4278 }
4279
4280 1
4281}
4282
4283#############################################################################
4284
4285package CFPlus::UI::HBox;
4286
4287our @ISA = CFPlus::UI::Box::;
4288
4289sub new {
4290 my $class = shift;
4291
4292 $class->SUPER::new (
4293 vertical => 0,
4294 @_,
839 ) 4295 )
840} 4296}
841 4297
842sub size_allocate {
843 my ($self, $x, $y, $w, $h) = @_;
844
845 $self->_size_allocate ($x, $y, $w, $h);
846
847 ($h, $w) = ($w, $h);
848
849 my $children = $self->{children};
850
851 my @h = map +($_->size_request)[0], @$children;
852
853 my $req_h = List::Util::sum @h;
854
855 if ($req_h > $h) {
856 # ah well, not enough space
857 $_ *= $h / $req_h for @h;
858 } else {
859 my $exp = List::Util::sum map $_->{expand}, @$children;
860 $exp ||= 1;
861
862 for (0 .. $#$children) {
863 my $child = $children->[$_];
864
865 my $alloc_h = $h[$_];
866 $alloc_h += ($h - $req_h) * $child->{expand} / $exp;
867 $h[$_] = $alloc_h;
868 }
869 }
870
871 CFClient::UI::harmonize \@h;
872
873 my $y = 0;
874 for (0 .. $#$children) {
875 my $child = $children->[$_];
876 my $h = $h[$_];
877 $child->size_allocate ($y, 0, $h, $w);
878
879 $y += $h;
880 }
881}
882
883############################################################################# 4298#############################################################################
884 4299
885package CFClient::UI::VBox; 4300package CFPlus::UI::VBox;
886 4301
887# TODO: wrap into common Box base class 4302our @ISA = CFPlus::UI::Box::;
888 4303
889our @ISA = CFClient::UI::Container::; 4304sub new {
4305 my $class = shift;
890 4306
891sub size_request { 4307 $class->SUPER::new (
892 my ($self) = @_; 4308 vertical => 1,
893 4309 @_,
894 my @alloc = map [$_->size_request], @{$self->{children}};
895
896 (
897 (List::Util::max map $_->[0], @alloc),
898 (List::Util::sum map $_->[1], @alloc),
899 ) 4310 )
900} 4311}
901 4312
902sub size_allocate {
903 my ($self, $x, $y, $w, $h) = @_;
904
905 $self->_size_allocate ($x, $y, $w, $h);
906
907 my $children = $self->{children};
908
909 my @h = map +($_->size_request)[1], @$children;
910
911 my $req_h = List::Util::sum @h;
912
913 if ($req_h > $h) {
914 # ah well, not enough space
915 $_ *= $h / $req_h for @h;
916 } else {
917 my $exp = List::Util::sum map $_->{expand}, @$children;
918 $exp ||= 1;
919
920 for (0 .. $#$children) {
921 my $child = $children->[$_];
922
923 $h[$_] += ($h - $req_h) * $child->{expand} / $exp;
924 }
925 }
926
927 CFClient::UI::harmonize \@h;
928
929 my $y = 0;
930 for (0 .. $#$children) {
931 my $child = $children->[$_];
932 my $h = $h[$_];
933 $child->size_allocate (0, $y, $w, $h);
934
935 $y += $h;
936 }
937}
938
939############################################################################# 4313#############################################################################
940 4314
941package CFClient::UI::Label; 4315package CFPlus::UI::Label;
942 4316
943our @ISA = CFClient::UI::Base::; 4317our @ISA = CFPlus::UI::DrawBG::;
944 4318
945use SDL::OpenGL; 4319use CFPlus::OpenGL;
946 4320
947sub new { 4321sub new {
948 my ($class, %arg) = @_; 4322 my ($class, %arg) = @_;
949 4323
950 my $self = $class->SUPER::new ( 4324 my $self = $class->SUPER::new (
951 fg => [1, 1, 1], 4325 fg => [1, 1, 1],
952 fontsize => $::FONTSIZE, 4326 #bg => none
953 text => "", 4327 #active_bg => none
4328 #font => default_font
4329 #text => initial text
4330 #markup => initial narkup
4331 #max_w => maximum pixel width
4332 #style => 0, # render flags
4333 ellipsise => 3, # end
4334 layout => (new CFPlus::Layout),
4335 fontsize => 1,
954 align => -1, 4336 align => -1,
4337 valign => -1,
955 padding => 2, 4338 padding_x => 2,
956 layout => new CFClient::Layout, 4339 padding_y => 2,
4340 can_events => 0,
957 %arg 4341 %arg
958 ); 4342 );
959 4343
4344 if (exists $self->{template}) {
4345 my $layout = new CFPlus::Layout;
4346 $layout->set_text (delete $self->{template});
4347 $self->{template} = $layout;
4348 }
4349
4350 if (exists $self->{markup}) {
4351 $self->set_markup (delete $self->{markup});
4352 } else {
960 $self->set_text ($self->{text}); 4353 $self->set_text (delete $self->{text});
4354 }
961 4355
962 $self 4356 $self
963} 4357}
964 4358
965sub escape_text { 4359sub update {
966 local $_ = $_[1]; 4360 my ($self) = @_;
967 4361
968 s/&/&amp;/g; 4362 delete $self->{texture};
969 s/>/&gt;/g; 4363 $self->SUPER::update;
970 s/</&lt;/g; 4364}
971 4365
972 $_[1] 4366sub realloc {
4367 my ($self) = @_;
4368
4369 delete $self->{ox};
4370 $self->SUPER::realloc;
973} 4371}
974 4372
975sub set_text { 4373sub set_text {
976 my ($self, $text) = @_; 4374 my ($self, $text) = @_;
977 4375
4376 return if $self->{text} eq "T$text";
978 $self->{text} = $text; 4377 $self->{text} = "T$text";
4378
4379 $self->{layout}->set_text ($text);
4380
4381 delete $self->{size_req};
4382 $self->realloc;
4383 $self->update;
4384}
4385
4386sub set_markup {
4387 my ($self, $markup) = @_;
4388
4389 return if $self->{text} eq "M$markup";
4390 $self->{text} = "M$markup";
4391
4392 my $rgba = $markup =~ /span.*(?:foreground|background)/;
4393
979 $self->{layout}->set_markup ($text); 4394 $self->{layout}->set_markup ($markup);
980 4395
4396 delete $self->{size_req};
4397 $self->realloc;
4398 $self->update;
4399}
4400
4401sub size_request {
4402 my ($self) = @_;
4403
4404 $self->{size_req} ||= do {
4405 $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
4413 if (exists $self->{template}) {
4414 $self->{template}->set_font ($self->{font}) if $self->{font};
4415 $self->{template}->set_width ($self->{max_w} || -1);
4416 $self->{template}->set_height ($self->{fontsize} * $::FONTSIZE);
4417
4418 my ($w2, $h2) = $self->{template}->size;
4419
4420 $w = List::Util::max $w, $w2;
4421 $h = List::Util::max $h, $h2;
4422 }
4423
4424 [$w, $h]
4425 };
4426
4427 @{ $self->{size_req} }
4428}
4429
4430sub baseline_shift {
4431 $_[0]{layout}->descent
4432}
4433
4434sub invoke_size_allocate {
4435 my ($self, $w, $h) = @_;
4436
4437 delete $self->{ox};
4438
4439 delete $self->{texture}
4440 unless $w >= $self->{req_w} && $self->{old_w} >= $self->{req_w};
4441
4442 1
4443}
4444
4445sub set_fontsize {
4446 my ($self, $fontsize) = @_;
4447
4448 $self->{fontsize} = $fontsize;
4449 delete $self->{size_req};
981 delete $self->{texture}; 4450 delete $self->{texture};
982# $self->{w} = $self->{h} = -1;
983 $self->update;
984}
985 4451
986sub get_text { 4452 $self->realloc;
4453}
4454
4455sub reconfigure {
987 my ($self, $text) = @_; 4456 my ($self) = @_;
988 4457
989 $self->{text} 4458 delete $self->{size_req};
990}
991
992sub size_request {
993 my ($self) = @_;
994
995 $self->{layout}->set_width;
996 $self->{layout}->set_height ($self->{fontsize});
997 my ($w, $h) = $self->{layout}->size;
998
999 (
1000 $w + $self->{padding} * 2,
1001 $h + $self->{padding} * 2,
1002 )
1003}
1004
1005sub size_allocate {
1006 my ($self, $x, $y, $w, $h) = @_;
1007
1008 $self->_size_allocate ($x, $y, $w, $h) or return;
1009
1010 delete $self->{texture}; 4459 delete $self->{texture};
1011}
1012 4460
1013sub update {
1014 my ($self) = @_;
1015
1016 delete $self->{texture};
1017 $self->SUPER::update; 4461 $self->SUPER::reconfigure;
1018} 4462}
1019 4463
1020sub _draw { 4464sub _draw {
1021 my ($self) = @_; 4465 my ($self) = @_;
1022 4466
4467 $self->SUPER::_draw; # draw background, if applicable
4468
1023 my $tex = $self->{texture} ||= do { 4469 my $size = $self->{texture} ||= do {
4470 $self->{layout}->set_foreground (@{$self->{fg}});
4471 $self->{layout}->set_font ($self->{font}) if $self->{font};
1024 $self->{layout}->set_width ($self->{w}); 4472 $self->{layout}->set_width ($self->{w});
1025 $self->{layout}->set_height (List::Util::min $self->{h} - $self->{padding} * 2, $self->{fontsize}); 4473 $self->{layout}->set_ellipsise ($self->{ellipsise});
1026 new_from_layout CFClient::Texture $self->{layout} 4474 $self->{layout}->set_single_paragraph_mode ($self->{ellipsise});
4475 $self->{layout}->set_height ($self->{fontsize} * $::FONTSIZE);
4476
4477 [$self->{layout}->size]
1027 }; 4478 };
1028 4479
1029 glEnable GL_BLEND; 4480 unless (exists $self->{ox}) {
1030 glBlendFunc GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA;
1031 glEnable GL_TEXTURE_2D;
1032 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
1033
1034 glColor @{$self->{fg}};
1035
1036 my $x =
1037 $self->{align} < 0 ? $self->{padding} 4481 $self->{ox} = int ($self->{align} < 0 ? $self->{padding_x}
1038 : $self->{align} > 0 ? $self->{w} - $tex->{w} - $self->{padding} 4482 : $self->{align} > 0 ? $self->{w} - $size->[0] - $self->{padding_x}
1039 : ($self->{w} - $tex->{w}) * 0.5; 4483 : ($self->{w} - $size->[0]) * 0.5);
1040 4484
1041 $tex->draw_quad (int $x, int +($self->{h} - $tex->{h}) * 0.5); 4485 $self->{oy} = int ($self->{valign} < 0 ? $self->{padding_y}
4486 : $self->{valign} > 0 ? $self->{h} - $size->[1] - $self->{padding_y}
4487 : ($self->{h} - $size->[1]) * 0.5);
4488 };
1042 4489
1043 glDisable GL_TEXTURE_2D; 4490 my $w = List::Util::min $self->{w} + 4, $size->[0];
1044 glDisable GL_BLEND; 4491 my $h = List::Util::min $self->{h} + 2, $size->[1];
4492
4493 $self->{layout}->render ($self->{ox}, $self->{oy}, $self->{style});
1045} 4494}
1046 4495
1047############################################################################# 4496#############################################################################
1048 4497
1049package CFClient::UI::Entry; 4498package CFPlus::UI::EntryBase;
1050 4499
1051our @ISA = CFClient::UI::Label::; 4500our @ISA = CFPlus::UI::Label::;
1052 4501
1053use SDL; 4502use CFPlus::OpenGL;
1054use SDL::OpenGL;
1055 4503
1056sub new { 4504sub new {
1057 my $class = shift; 4505 my $class = shift;
1058 4506
1059 $class->SUPER::new ( 4507 $class->SUPER::new (
1060 fg => [1, 1, 1], 4508 fg => [1, 1, 1],
1061 bg => [0, 0, 0, 0.2], 4509 bg => [0, 0, 0, 0.2],
1062 active_bg => [1, 1, 1, 0.5], 4510 active_bg => [1, 1, 1, 0.5],
1063 active_fg => [0, 0, 0], 4511 active_fg => [0, 0, 0],
1064 can_hover => 1, 4512 can_hover => 1,
1065 can_focus => 1, 4513 can_focus => 1,
4514 valign => 0,
4515 can_events => 1,
4516 ellipsise => 0,
4517 #text => ...
4518 #hidden => "*",
1066 @_ 4519 @_
1067 ) 4520 )
1068} 4521}
1069 4522
1070sub _set_text { 4523sub _set_text {
1071 my ($self, $text) = @_; 4524 my ($self, $text) = @_;
1072 4525
1073 my $old_text = $self->{text}; 4526 delete $self->{cur_h};
4527
4528 return if $self->{text} eq $text;
1074 4529
1075 $self->{last_activity} = $::NOW; 4530 $self->{last_activity} = $::NOW;
1076
1077 $self->{text} = $text; 4531 $self->{text} = $text;
1078 $self->{layout}->set_width ($self->{w});
1079 $self->{layout}->set_height (List::Util::min $self->{h} - $self->{padding} * 2, $self->{fontsize});
1080 4532
1081 $text =~ s/./*/g if $self->{hidden}; 4533 $text =~ s/./*/g if $self->{hidden};
4534 $self->{layout}->set_text ("$text ");
4535 delete $self->{size_req};
1082 4536
1083 $self->{layout}->set_markup ($self->escape_text ($text) . " "); 4537 $self->emit (changed => $self->{text});
1084 4538
1085 $text = substr $text, 0, $self->{cursor}; 4539 $self->realloc;
1086 utf8::encode $text; 4540 $self->update;
1087
1088 @$self{qw(cur_x cur_y cur_h)} = $self->{layout}->cursor_pos (length $text);
1089
1090 $self->emit (changed => $self->{text}) # XXX: is this the right place to do this?
1091 if $old_text ne $self->{text};
1092}
1093
1094sub size_request {
1095 my ($self) = @_;
1096
1097 my ($w, $h) = $self->SUPER::size_request;
1098
1099 ($w + 1, $h) # add 1 for cursor
1100}
1101
1102sub size_allocate {
1103 my ($self, $x, $y, $w, $h) = @_;
1104
1105 $self->SUPER::size_allocate ($x, $y, $w, $h);
1106
1107 $self->_set_text ($self->{text});
1108} 4541}
1109 4542
1110sub set_text { 4543sub set_text {
1111 my ($self, $text) = @_; 4544 my ($self, $text) = @_;
1112 4545
1113 $self->{cursor} = length $text; 4546 $self->{cursor} = length $text;
1114 $self->_set_text ($text); 4547 $self->_set_text ($text);
4548}
4549
4550sub get_text {
4551 $_[0]{text}
4552}
4553
4554sub 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
4562sub invoke_key_down {
4563 my ($self, $ev) = @_;
4564
4565 my $mod = $ev->{mod};
4566 my $sym = $ev->{sym};
4567 my $uni = $ev->{unicode};
4568
4569 my $text = $self->get_text;
4570
4571 $self->{cursor} = List::Util::max 0, List::Util::min $self->{cursor}, length $text;
4572
4573 if ($uni == 8) {
4574 substr $text, --$self->{cursor}, 1, "" if $self->{cursor};
4575 } elsif ($uni == 127) {
4576 substr $text, $self->{cursor}, 1, "";
4577 } elsif ($sym == CFPlus::SDLK_LEFT) {
4578 --$self->{cursor} if $self->{cursor};
4579 } elsif ($sym == CFPlus::SDLK_RIGHT) {
4580 ++$self->{cursor} if $self->{cursor} < length $self->{text};
4581 } elsif ($sym == CFPlus::SDLK_HOME) {
4582 # what a hack
4583 $self->{cursor} =
4584 (substr $self->{text}, 0, $self->{cursor}) =~ /^(.*\012)/
4585 ? length $1
4586 : 0;
4587 } elsif ($sym == CFPlus::SDLK_END) {
4588 # uh, again
4589 $self->{cursor} =
4590 (substr $self->{text}, $self->{cursor}) =~ /^([^\012]*)\012/
4591 ? $self->{cursor} + length $1
4592 : length $self->{text};
4593 } elsif ($uni == 21) { # ctrl-u
4594 $text = "";
4595 $self->{cursor} = 0;
4596 } elsif ($uni == 27) {
4597 $self->emit ('escape');
4598 } elsif ($uni == 0x0d) {
4599 substr $text, $self->{cursor}++, 0, "\012";
4600 } elsif ($uni >= 0x20) {
4601 substr $text, $self->{cursor}++, 0, chr $uni;
4602 } else {
4603 return 0;
4604 }
4605
4606 $self->_set_text ($text);
4607
4608 $self->realloc;
1115 $self->update; 4609 $self->update;
1116}
1117 4610
1118sub key_down {
1119 my ($self, $ev) = @_;
1120
1121 my $mod = $ev->key_mod;
1122 my $sym = $ev->key_sym;
1123
1124 my $uni = $ev->key_unicode;
1125
1126 my $text = $self->get_text;
1127
1128 if ($sym == SDLK_BACKSPACE) {
1129 substr $text, --$self->{cursor}, 1, "" if $self->{cursor};
1130 } elsif ($sym == SDLK_DELETE) {
1131 substr $text, $self->{cursor}, 1, "";
1132 } elsif ($sym == SDLK_LEFT) {
1133 --$self->{cursor} if $self->{cursor};
1134 } elsif ($sym == SDLK_RIGHT) {
1135 ++$self->{cursor} if $self->{cursor} < length $self->{text};
1136 } elsif ($sym == SDLK_HOME) {
1137 $self->{cursor} = 0;
1138 } elsif ($sym == SDLK_END) {
1139 $self->{cursor} = length $text;
1140 } elsif ($sym == SDLK_ESCAPE) {
1141 $self->emit ('escape');
1142 } elsif ($uni) {
1143 substr $text, $self->{cursor}++, 0, chr $uni;
1144 } 4611 1
1145
1146 $self->_set_text ($text);
1147 $self->update;
1148} 4612}
1149 4613
1150sub focus_in { 4614sub invoke_focus_in {
1151 my ($self) = @_; 4615 my ($self) = @_;
1152 4616
1153 $self->{last_activity} = $::NOW; 4617 $self->{last_activity} = $::NOW;
1154 4618
1155 $self->SUPER::focus_in; 4619 $self->SUPER::invoke_focus_in
1156} 4620}
1157 4621
1158sub button_down { 4622sub invoke_button_down {
1159 my ($self, $ev, $x, $y) = @_; 4623 my ($self, $ev, $x, $y) = @_;
1160 4624
1161 $self->SUPER::button_down ($ev, $x, $y); 4625 $self->SUPER::invoke_button_down ($ev, $x, $y);
1162 4626
1163 my $idx = $self->{layout}->xy_to_index ($x, $y); 4627 my $idx = $self->{layout}->xy_to_index ($x, $y);
1164 4628
1165 # byte-index to char-index 4629 # byte-index to char-index
1166 my $text = $self->{text}; 4630 my $text = $self->{text};
1167 utf8::encode $text; 4631 utf8::encode $text; $text = substr $text, 0, $idx; utf8::decode $text;
1168 $self->{cursor} = length substr $text, 0, $idx; 4632 $self->{cursor} = length $text;
1169 4633
1170 $self->_set_text ($self->{text}); 4634 $self->_set_text ($self->{text});
1171 $self->update; 4635 $self->update;
4636
4637 1
1172} 4638}
1173 4639
1174sub mouse_motion { 4640sub invoke_mouse_motion {
1175 my ($self, $ev, $x, $y) = @_; 4641 my ($self, $ev, $x, $y) = @_;
1176# printf "M %d,%d %d,%d\n", $ev->motion_x, $ev->motion_y, $x, $y;#d# 4642# printf "M %d,%d %d,%d\n", $ev->motion_x, $ev->motion_y, $x, $y;#d#
4643
4644 1
1177} 4645}
1178 4646
1179sub _draw { 4647sub _draw {
1180 my ($self) = @_; 4648 my ($self) = @_;
1181 4649
1182 local $self->{fg} = $self->{fg}; 4650 local $self->{fg} = $self->{fg};
1183 4651
1184 if ($FOCUS == $self) { 4652 if ($FOCUS == $self) {
1185 glColor @{$self->{active_bg}}; 4653 glColor_premultiply @{$self->{active_bg}};
1186 $self->{fg} = $self->{active_fg}; 4654 $self->{fg} = $self->{active_fg};
1187 } else { 4655 } else {
1188 glColor @{$self->{bg}}; 4656 glColor_premultiply @{$self->{bg}};
1189 } 4657 }
1190 4658
1191 glEnable GL_BLEND; 4659 glEnable GL_BLEND;
1192 glBlendFunc GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA; 4660 glBlendFunc GL_ONE, GL_ONE_MINUS_SRC_ALPHA;
1193 glBegin GL_QUADS; 4661 glBegin GL_QUADS;
1194 glVertex 0 , 0; 4662 glVertex 0 , 0;
1195 glVertex 0 , $self->{h}; 4663 glVertex 0 , $self->{h};
1196 glVertex $self->{w}, $self->{h}; 4664 glVertex $self->{w}, $self->{h};
1197 glVertex $self->{w}, 0; 4665 glVertex $self->{w}, 0;
1200 4668
1201 $self->SUPER::_draw; 4669 $self->SUPER::_draw;
1202 4670
1203 #TODO: force update every cursor change :( 4671 #TODO: force update every cursor change :(
1204 if ($FOCUS == $self && (($::NOW - $self->{last_activity}) & 1023) < 600) { 4672 if ($FOCUS == $self && (($::NOW - $self->{last_activity}) & 1023) < 600) {
1205 glColor @{$self->{fg}}; 4673
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
1206 glBegin GL_LINES; 4681 glBegin GL_LINES;
1207 glVertex $self->{cur_x}, $self->{cur_y};
1208 glVertex $self->{cur_x}, $self->{cur_y} + $self->{cur_h}; 4682 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};
1209 glEnd; 4684 glEnd;
1210 } 4685 }
1211} 4686}
1212 4687
4688#############################################################################
4689
1213package CFClient::UI::LineEntry; 4690package CFPlus::UI::Entry;
1214 4691
1215our @ISA = CFClient::UI::Entry::; 4692our @ISA = CFPlus::UI::EntryBase::;
1216 4693
1217use SDL; 4694use CFPlus::OpenGL;
1218use SDL::OpenGL;
1219 4695
1220sub key_down { 4696sub invoke_key_down {
1221 my ($self, $ev) = @_; 4697 my ($self, $ev) = @_;
1222 4698
1223 my $sym = $ev->key_sym; 4699 my $sym = $ev->{sym};
1224 4700
1225 if ($sym == SDLK_RETURN) { 4701 if ($ev->{uni} == 0x0d || $sym == 13) {
4702 unshift @{$self->{history}},
4703 my $txt = $self->get_text;
4704
4705 $self->{history_pointer} = -1;
4706 $self->{history_saveback} = '';
1226 $self->emit (activate => $self->get_text); 4707 $self->emit (activate => $txt);
1227 $self->update; 4708 $self->update;
1228 4709
4710 } elsif ($sym == CFPlus::SDLK_UP) {
4711 if ($self->{history_pointer} < 0) {
4712 $self->{history_saveback} = $self->get_text;
4713 }
4714 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 }
4721
4722 } elsif ($sym == CFPlus::SDLK_DOWN) {
4723 $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
1229 } else { 4732 } else {
1230 $self->SUPER::key_down ($ev); 4733 return $self->SUPER::invoke_key_down ($ev)
4734 }
4735
1231 } 4736 1
1232
1233} 4737}
1234 4738
1235############################################################################# 4739#############################################################################
1236 4740
4741package CFPlus::UI::TextEdit;
4742
4743our @ISA = CFPlus::UI::EntryBase::;
4744
4745use CFPlus::OpenGL;
4746
4747sub move_cursor_ver {
4748 my ($self, $dy) = @_;
4749
4750 my ($y, $x) = $self->{layout}->index_to_line_x ($self->{cursor});
4751
4752 $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}
4761
4762sub 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
1237package CFClient::UI::Button; 4780package CFPlus::UI::Button;
1238 4781
1239our @ISA = CFClient::UI::Label::; 4782our @ISA = CFPlus::UI::Label::;
1240 4783
1241use SDL; 4784use CFPlus::OpenGL;
1242use SDL::OpenGL;
1243 4785
1244my @tex = 4786my @tex =
1245 map { new_from_file CFClient::Texture CFClient::find_rcfile $_ } 4787 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
1246 qw(b1_button_active.png); 4788 qw(b1_button_inactive.png b1_button_active.png);
1247 4789
1248sub new { 4790sub new {
1249 my $class = shift; 4791 my $class = shift;
1250 4792
1251 $class->SUPER::new ( 4793 $class->SUPER::new (
1252 padding => 4, 4794 padding_x => 4,
4795 padding_y => 4,
1253 fg => [1, 1, 1], 4796 fg => [1.0, 1.0, 1.0],
1254 bg => [1, 1, 1, 0.2],
1255 active_fg => [1, 1, 0], 4797 active_fg => [0.8, 0.8, 0.8],
1256 can_hover => 1, 4798 can_hover => 1,
4799 align => 0,
4800 valign => 0,
4801 can_events => 1,
1257 @_ 4802 @_
1258 ) 4803 )
1259} 4804}
1260 4805
1261sub button_up { 4806sub invoke_button_up {
1262 my ($self, $ev, $x, $y) = @_; 4807 my ($self, $ev, $x, $y) = @_;
1263 4808
4809 $self->emit ("activate")
1264 if ($x >= 0 && $x < $self->{w} 4810 if $x >= 0 && $x < $self->{w}
1265 && $y >= 0 && $y < $self->{h}) { 4811 && $y >= 0 && $y < $self->{h};
1266 $self->emit ("activate"); 4812
1267 } 4813 1
1268} 4814}
1269 4815
1270sub _draw { 4816sub _draw {
1271 my ($self) = @_; 4817 my ($self) = @_;
1272 4818
1273 local $self->{fg} = $self->{fg}; 4819 local $self->{fg} = $GRAB == $self ? $self->{active_fg} : $self->{fg};
1274 my $tex = $tex[0];
1275 4820
1276 glEnable GL_BLEND;
1277 glEnable GL_TEXTURE_2D; 4821 glEnable GL_TEXTURE_2D;
1278 glBlendFunc GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA;
1279
1280 if ($GRAB == $self) {
1281 $self->{fg} = $self->{active_fg};
1282 }
1283
1284 glBindTexture GL_TEXTURE_2D, $tex->{name};
1285 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE; 4822 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
4823 glColor 0, 0, 0, 1;
1286 4824
4825 my $tex = $tex[$GRAB == $self];
1287 $tex->draw_quad (0, 0, $self->{w}, $self->{h}); 4826 $tex->draw_quad_alpha (0, 0, $self->{w}, $self->{h});
1288 4827
1289 glDisable GL_TEXTURE_2D; 4828 glDisable GL_TEXTURE_2D;
1290 glDisable GL_BLEND;
1291 4829
1292 $self->SUPER::_draw; 4830 $self->SUPER::_draw;
1293} 4831}
1294 4832
1295############################################################################# 4833#############################################################################
1296 4834
1297package CFClient::UI::CheckBox; 4835package CFPlus::UI::CheckBox;
1298 4836
1299our @ISA = CFClient::UI::DrawBG::; 4837our @ISA = CFPlus::UI::DrawBG::;
1300 4838
1301my @tex = 4839my @tex =
1302 map { new_from_file CFClient::Texture CFClient::find_rcfile $_ } 4840 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
1303 qw(c1_checkbox_bg.png c1_checkbox_active.png); 4841 qw(c1_checkbox_bg.png c1_checkbox_active.png);
1304 4842
1305use SDL; 4843use CFPlus::OpenGL;
1306use SDL::OpenGL;
1307 4844
1308sub new { 4845sub new {
1309 my $class = shift; 4846 my $class = shift;
1310 4847
1311 $class->SUPER::new ( 4848 $class->SUPER::new (
1312 padding => 2, 4849 padding_x => 2,
4850 padding_y => 2,
1313 fg => [1, 1, 1], 4851 fg => [1, 1, 1],
1314 active_fg => [1, 1, 0], 4852 active_fg => [1, 1, 0],
4853 bg => [0, 0, 0, 0.2],
4854 active_bg => [1, 1, 1, 0.5],
1315 state => 0, 4855 state => 0,
1316 can_hover => 1, 4856 can_hover => 1,
1317 @_ 4857 @_
1318 ) 4858 )
1319} 4859}
1320 4860
1321sub size_request { 4861sub size_request {
1322 my ($self) = @_; 4862 my ($self) = @_;
1323 4863
1324 ($self->{padding} * 2 + 6) x 2 4864 (6) x 2
1325} 4865}
1326 4866
1327sub size_allocate { 4867sub toggle {
1328 my ($self, $x, $y, $w, $h) = @_; 4868 my ($self) = @_;
1329 4869
1330 $self->_size_allocate ($x, $y, $w, $h); 4870 $self->{state} = !$self->{state};
4871 $self->emit (changed => $self->{state});
4872 $self->update;
1331} 4873}
1332 4874
1333sub button_down { 4875sub invoke_button_down {
1334 my ($self, $ev, $x, $y) = @_; 4876 my ($self, $ev, $x, $y) = @_;
1335 4877
1336 if ($x >= $self->{padding} && $x < $self->{w} - $self->{padding} 4878 if ($x >= $self->{padding_x} && $x < $self->{w} - $self->{padding_x}
1337 && $y >= $self->{padding} && $y < $self->{h} - $self->{padding}) { 4879 && $y >= $self->{padding_y} && $y < $self->{h} - $self->{padding_y}) {
1338 $self->{state} = !$self->{state}; 4880 $self->toggle;
1339 $self->emit (changed => $self->{state}); 4881 } else {
4882 return 0
4883 }
4884
1340 } 4885 1
1341} 4886}
1342 4887
1343sub _draw { 4888sub _draw {
1344 my ($self) = @_; 4889 my ($self) = @_;
1345 4890
1346 $self->SUPER::_draw; 4891 $self->SUPER::_draw;
1347 4892
1348 glTranslate $self->{padding} + 0.375, $self->{padding} + 0.375, 0; 4893 glTranslate $self->{padding_x} + 0.375, $self->{padding_y} + 0.375, 0;
1349 4894
1350 my $s = (List::Util::min @$self{qw(w h)}) - $self->{padding} * 2; 4895 my ($w, $h) = @$self{qw(w h)};
4896
4897 my $s = List::Util::min $w - $self->{padding_x} * 2, $h - $self->{padding_y} * 2;
1351 4898
1352 glColor @{ $FOCUS == $self ? $self->{active_fg} : $self->{fg} }; 4899 glColor @{ $FOCUS == $self ? $self->{active_fg} : $self->{fg} };
1353 4900
1354 glEnable GL_BLEND; 4901 my $tex = $self->{state} ? $tex[1] : $tex[0];
4902
1355 glEnable GL_TEXTURE_2D; 4903 glEnable GL_TEXTURE_2D;
1356 glBlendFunc GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA;
1357
1358 my $tex = $self->{state} ? $tex[1] : $tex[0];
1359
1360 $tex->draw_quad (0, 0, $s, $s); 4904 $tex->draw_quad_alpha (0, 0, $s, $s);
1361
1362 glDisable GL_TEXTURE_2D; 4905 glDisable GL_TEXTURE_2D;
1363 glDisable GL_BLEND;
1364} 4906}
1365 4907
1366############################################################################# 4908#############################################################################
1367 4909
1368package CFClient::UI::Slider; 4910package CFPlus::UI::Image;
1369 4911
1370use strict;
1371
1372use SDL::OpenGL;
1373
1374our @ISA = CFClient::UI::DrawBG::; 4912our @ISA = CFPlus::UI::Base::;
1375 4913
1376my @tex = 4914use CFPlus::OpenGL;
1377 map { new_from_file CFClient::Texture CFClient::find_rcfile $_ } 4915
1378 qw(s1_slider.png s1_slider_bg.png); 4916our %texture_cache;
1379 4917
1380sub new { 4918sub new {
1381 my $class = shift; 4919 my $class = shift;
1382 4920
4921 my $self = $class->SUPER::new (
4922 can_events => 0,
4923 @_,
4924 );
4925
4926 $self->{path} || $self->{tex}
4927 or Carp::croak "'path' or 'tex' attributes required";
4928
4929 $self->{tex} ||= $texture_cache{$self->{path}} ||=
4930 new_from_file CFPlus::Texture CFPlus::find_rcfile $self->{path}, mipmap => 1;
4931
4932 CFPlus::weaken $texture_cache{$self->{path}};
4933
4934 $self->{aspect} ||= $self->{tex}{w} / $self->{tex}{h};
4935
4936 $self
4937}
4938
4939sub STORABLE_freeze {
4940 my ($self, $cloning) = @_;
4941
4942 $self->{path}
4943 or die "cannot serialise CFPlus::UI::Image on non-loadable images\n";
4944
4945 $self->{path}
4946}
4947
4948sub STORABLE_attach {
4949 my ($self, $cloning, $path) = @_;
4950
4951 $self->new (path => $path)
4952}
4953
4954sub size_request {
4955 my ($self) = @_;
4956
4957 ($self->{tex}{w}, $self->{tex}{h})
4958}
4959
4960sub _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 $tex->draw_quad (0, 0, $w, $h);
4978
4979 glDisable GL_TEXTURE_2D;
4980}
4981
4982#############################################################################
4983
4984package CFPlus::UI::ImageButton;
4985
4986our @ISA = CFPlus::UI::Image::;
4987
4988use CFPlus::OpenGL;
4989
4990my %textures;
4991
4992sub 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
5008sub 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
5020package CFPlus::UI::VGauge;
5021
5022our @ISA = CFPlus::UI::Base::;
5023
5024use List::Util qw(min max);
5025
5026use CFPlus::OpenGL;
5027
5028my %tex = (
5029 food => [
5030 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
5031 qw/g1_food_gauge_empty.png g1_food_gauge_full.png/
5032 ],
5033 grace => [
5034 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
5035 qw/g1_grace_gauge_empty.png g1_grace_gauge_full.png g1_grace_gauge_overflow.png/
5036 ],
5037 hp => [
5038 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
5039 qw/g1_hp_gauge_empty.png g1_hp_gauge_full.png/
5040 ],
5041 mana => [
5042 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
5043 qw/g1_mana_gauge_empty.png g1_mana_gauge_full.png g1_mana_gauge_overflow.png/
5044 ],
5045);
5046
5047# eg. VGauge->new (gauge => 'food'), default gauge: food
5048sub new {
5049 my $class = shift;
5050
5051 my $self = $class->SUPER::new (
5052 type => 'food',
5053 @_
5054 );
5055
5056 $self->{aspect} = $tex{$self->{type}}[0]{w} / $tex{$self->{type}}[0]{h};
5057
5058 $self
5059}
5060
5061sub size_request {
5062 my ($self) = @_;
5063
5064 #my $tex = $tex{$self->{type}}[0];
5065 #@$tex{qw(w h)}
5066 (0, 0)
5067}
5068
5069sub set_max {
5070 my ($self, $max) = @_;
5071
5072 return if $self->{max_val} == $max;
5073
5074 $self->{max_val} = $max;
5075 $self->update;
5076}
5077
5078sub set_value {
5079 my ($self, $val, $max) = @_;
5080
5081 $self->set_max ($max)
5082 if defined $max;
5083
5084 return if $self->{val} == $val;
5085
5086 $self->{val} = $val;
5087 $self->update;
5088}
5089
5090sub _draw {
5091 my ($self) = @_;
5092
5093 my $tex = $tex{$self->{type}};
5094 my ($t1, $t2, $t3) = @$tex;
5095
5096 my ($w, $h) = ($self->{w}, $self->{h});
5097
5098 if ($self->{vertical}) {
5099 glRotate 90, 0, 0, 1;
5100 glTranslate 0, -$self->{w}, 0;
5101
5102 ($w, $h) = ($h, $w);
5103 }
5104
5105 my $ycut = $self->{val} / ($self->{max_val} || 1);
5106
5107 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 my $h3 = $self->{h};
5113
5114 $_ = $_ * (284-4)/288 + 4/288 for ($h1, $h2, $h3);
5115
5116 glEnable GL_BLEND;
5117 glBlendFuncSeparate GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
5118 GL_ONE, GL_ONE_MINUS_SRC_ALPHA;
5119 glEnable GL_TEXTURE_2D;
5120 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
5121
5122 glBindTexture GL_TEXTURE_2D, $t1->{name};
5123 glBegin GL_QUADS;
5124 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 glEnd;
5129
5130 my $ycut1 = List::Util::min 1, $ycut;
5131 glBindTexture GL_TEXTURE_2D, $t2->{name};
5132 glBegin GL_QUADS;
5133 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 glEnd;
5138
5139 if ($t3) {
5140 glBindTexture GL_TEXTURE_2D, $t3->{name};
5141 glBegin GL_QUADS;
5142 glTexCoord 0 , $t3->{t} * (1 - $ycut2); glVertex 0 , $h2;
5143 glTexCoord 0 , $t3->{t}; glVertex 0 , $h3;
5144 glTexCoord $t3->{s}, $t3->{t}; glVertex $w, $h3;
5145 glTexCoord $t3->{s}, $t3->{t} * (1 - $ycut2); glVertex $w, $h2;
5146 glEnd;
5147 }
5148
5149 glDisable GL_BLEND;
5150 glDisable GL_TEXTURE_2D;
5151}
5152
5153#############################################################################
5154
5155package CFPlus::UI::Gauge;
5156
5157our @ISA = CFPlus::UI::VBox::;
5158
5159sub new {
5160 my ($class, %arg) = @_;
5161
5162 my $self = $class->SUPER::new (
5163 tooltip => $arg{type},
5164 can_hover => 1,
5165 can_events => 1,
5166 %arg,
5167 );
5168
5169 $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
5173 $self
5174}
5175
5176sub set_fontsize {
5177 my ($self, $fsize) = @_;
5178
5179 $self->{value}->set_fontsize ($fsize);
5180 $self->{max} ->set_fontsize ($fsize);
5181}
5182
5183sub set_max {
5184 my ($self, $max) = @_;
5185
5186 $self->{gauge}->set_max ($max);
5187 $self->{max}->set_text ($max);
5188}
5189
5190sub 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
5202package CFPlus::UI::Slider;
5203
5204use strict;
5205
5206use CFPlus::OpenGL;
5207
5208our @ISA = CFPlus::UI::DrawBG::;
5209
5210my @tex =
5211 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_ }
5212 qw(s1_slider.png s1_slider_bg.png);
5213
5214sub new {
5215 my $class = shift;
5216
1383 # range [value, low, high, page] 5217 # range [value, low, high, page, unit]
1384 5218
1385 # TODO: 0-width page 5219 # TODO: 0-width page
1386 # TODO: req_w/h are wrong with vertical 5220 # TODO: req_w/h are wrong with vertical
1387 # TODO: calculations are off 5221 # TODO: calculations are off
1388 my $self = $class->SUPER::new ( 5222 my $self = $class->SUPER::new (
1389 fg => [1, 1, 1], 5223 fg => [1, 1, 1],
1390 active_fg => [0, 0, 0], 5224 active_fg => [0, 0, 0],
5225 bg => [0, 0, 0, 0.2],
5226 active_bg => [1, 1, 1, 0.5],
1391 range => [0, 0, 100, 10], 5227 range => [0, 0, 100, 10, 0],
1392 req_w => 40, 5228 min_w => $::WIDTH / 80,
1393 req_h => 13, 5229 min_h => $::WIDTH / 80,
1394 vertical => 0, 5230 vertical => 0,
1395 can_hover => 1, 5231 can_hover => 1,
1396 inner_pad => 5, 5232 inner_pad => 0.02,
1397 @_ 5233 @_
1398 ); 5234 );
1399 5235
5236 $self->set_value ($self->{range}[0]);
5237 $self->update;
5238
1400 $self 5239 $self
1401} 5240}
1402 5241
5242sub set_range {
5243 my ($self, $range) = @_;
5244
5245 ($range, $self->{range}) = ($self->{range}, $range);
5246
5247 if ("@$range" ne "@{$self->{range}}") {
5248 $self->update;
5249 $self->set_value ($self->{range}[0]);
5250 }
5251}
5252
5253sub 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 $page = $hi - $lo if $page > $hi - $lo;
5261
5262 $value = $lo if $value < $lo;
5263 $value = $hi - $page if $value > $hi - $page;
5264
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 $self->emit (changed => $value);
5272 $self->update;
5273 }
5274}
5275
1403sub size_request { 5276sub size_request {
1404 my ($self) = @_; 5277 my ($self) = @_;
1405 5278
1406 my $w = $self->{req_w}; 5279 ($self->{req_w}, $self->{req_h})
1407 my $h = $self->{req_h};
1408
1409 $self->{vertical} ? ($h, $w) : ($w, $h)
1410} 5280}
1411 5281
1412sub button_down { 5282sub invoke_button_down {
1413 my ($self, $ev, $x, $y) = @_; 5283 my ($self, $ev, $x, $y) = @_;
1414 5284
1415 $self->SUPER::button_down ($ev, $x, $y); 5285 $self->SUPER::invoke_button_down ($ev, $x, $y);
5286
5287 $self->{click} = [$self->{range}[0], $self->{vertical} ? $y : $x];
5288
1416 $self->mouse_motion ($ev, $x, $y); 5289 $self->invoke_mouse_motion ($ev, $x, $y)
1417} 5290}
1418 5291
1419sub mouse_motion { 5292sub invoke_mouse_motion {
1420 my ($self, $ev, $x, $y) = @_; 5293 my ($self, $ev, $x, $y) = @_;
1421 5294
1422 if ($GRAB == $self) { 5295 if ($GRAB == $self) {
5296 my ($x, $w) = $self->{vertical} ? ($y, $self->{h}) : ($x, $self->{w});
5297
5298 my (undef, $lo, $hi, $page) = @{$self->{range}};
5299
5300 $x = ($x - $self->{click}[1]) / ($w * $self->{scale});
5301
5302 $self->set_value ($self->{click}[0] + $x * ($hi - $page - $lo));
5303 } else {
5304 return 0;
5305 }
5306
5307 1
5308}
5309
5310sub invoke_mouse_wheel {
5311 my ($self, $ev) = @_;
5312
5313 my $delta = $self->{vertical} ? $ev->{dy} : $ev->{dx};
5314
5315 my $pagepart = $ev->{mod} & CFPlus::KMOD_SHIFT ? 1 : 0.2;
5316
5317 $self->set_value ($self->{range}[0] + $delta * $self->{range}[3] * $pagepart);
5318
5319 ! ! $delta
5320}
5321
5322sub update {
5323 my ($self) = @_;
5324
5325 delete $self->{knob_w};
5326 $self->SUPER::update;
5327}
5328
5329sub _draw {
5330 my ($self) = @_;
5331
5332 unless ($self->{knob_w}) {
5333 $self->set_value ($self->{range}[0]);
5334
1423 my ($value, $lo, $hi, $page) = @{$self->{range}}; 5335 my ($value, $lo, $hi, $page) = @{$self->{range}};
5336 my $range = ($hi - $page - $lo) || 1e-100;
1424 5337
1425 my ($x, $w) = $self->{vertical} ? ($y, $self->{h}) : ($x, $self->{w}); 5338 my $knob_w = List::Util::min 1, $page / ($hi - $lo) || 0.1;
1426 5339
1427 my $inner_pad_px = $self->_calc_inner_pad_px ($w); 5340 $self->{offset} = List::Util::max $self->{inner_pad}, $knob_w * 0.5;
1428 my $inner_w = $w - $inner_pad_px * 2; # * 2 for left & right 5341 $self->{scale} = 1 - 2 * $self->{offset} || 1e-100;
1429 5342
1430 $x -= $inner_pad_px; # substract the padding 5343 $value = ($value - $lo) / $range;
1431 $x = $x * ($hi - $lo) / $inner_w + $lo; 5344 $value = $value * $self->{scale} + $self->{offset};
1432 $x = $lo if $x < $lo;
1433 $x = $hi - $page if $x > $hi - $page;
1434 $self->{range}[0] = $x;
1435 5345
1436 $self->emit (changed => $x); 5346 $self->{knob_x} = $value - $knob_w * 0.5;
1437 $self->update; 5347 $self->{knob_w} = $knob_w;
1438 } 5348 }
1439}
1440
1441# the inner_* stuff is for generating a padding for the slider handle,
1442# so that the handle doesn't leave the texture. This calculation isn't 100%
1443# correct propably, but it does the job for now
1444sub _calc_inner_pad_px {
1445 my ($self, $w) = @_;
1446 ($w / 100) * $self->{inner_pad} # % to pixels
1447}
1448
1449sub _draw {
1450 my ($self) = @_;
1451 5349
1452 $self->SUPER::_draw (); 5350 $self->SUPER::_draw ();
1453 5351
1454 my ($w, $h) = @$self{qw(w h)}; 5352 glScale $self->{w}, $self->{h};
1455 5353
1456 if ($self->{vertical}) { 5354 if ($self->{vertical}) {
1457 # draw a vertical slider like a rotated horizontal slider 5355 # draw a vertical slider like a rotated horizontal slider
1458 5356
5357 glTranslate 1, 0, 0;
1459 glRotate 90, 0, 0, 1; 5358 glRotate 90, 0, 0, 1;
1460 glTranslate 0, -$self->{w}, 0;
1461
1462 ($w, $h) = ($h, $w);
1463 } 5359 }
1464 5360
1465 my $fg = $FOCUS == $self ? $self->{active_fg} : $self->{fg}; 5361 my $fg = $FOCUS == $self ? $self->{active_fg} : $self->{fg};
1466 my $bg = $FOCUS == $self ? $self->{active_bg} : $self->{bg}; 5362 my $bg = $FOCUS == $self ? $self->{active_bg} : $self->{bg};
1467 5363
1468 my ($value, $lo, $hi, $page) = @{$self->{range}};
1469
1470 $hi = $value + 1 if $lo == $hi;
1471
1472 my $inner_pad_px = $self->_calc_inner_pad_px ($w);
1473 my $inner_w = $w - $inner_pad_px * 2; # * 2 for left & right
1474
1475 $page = int $page * $inner_w / ($hi - $lo);
1476 $value = int +($value - $lo) * $inner_w / ($hi - $lo);
1477
1478 $w -= $page;
1479 $page &= ~1;
1480 glTranslate $page * 0.5, 0, 0;
1481 $page ||= 2;
1482
1483 my $knob_a = $inner_pad_px + ($value - $page * 0.5);
1484 my $knob_b = $inner_pad_px + ($value + $page * 0.5);
1485
1486 glEnable GL_BLEND;
1487 glBlendFunc GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA;
1488 glEnable GL_TEXTURE_2D; 5364 glEnable GL_TEXTURE_2D;
1489 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE; 5365 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
1490 5366
1491 # draw background 5367 # draw background
1492 $tex[1]->draw_quad (0, 0, $w, $h); 5368 $tex[1]->draw_quad_alpha (0, 0, 1, 1);
1493 5369
1494 # draw handle 5370 # draw handle
1495 $tex[0]->draw_quad ($knob_a, 0, $knob_b - $knob_a, $h); 5371 $tex[0]->draw_quad_alpha ($self->{knob_x}, 0, $self->{knob_w}, 1);
1496 5372
1497 glDisable GL_BLEND;
1498 glDisable GL_TEXTURE_2D; 5373 glDisable GL_TEXTURE_2D;
1499} 5374}
1500 5375
1501############################################################################# 5376#############################################################################
1502 5377
1503package CFClient::UI::TextView; 5378package CFPlus::UI::ValSlider;
1504 5379
1505our @ISA = CFClient::UI::HBox::; 5380our @ISA = CFPlus::UI::HBox::;
1506 5381
1507use SDL::OpenGL; 5382sub new {
5383 my ($class, %arg) = @_;
5384
5385 my $range = delete $arg{range};
5386
5387 my $self = $class->SUPER::new (
5388 slider => (new CFPlus::UI::Slider expand => 1, range => $range),
5389 entry => (new CFPlus::UI::Label text => "", template => delete $arg{template}),
5390 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
5414sub set_range { shift->{slider}->set_range (@_) }
5415sub set_value { shift->{slider}->set_value (@_) }
5416
5417#############################################################################
5418
5419package CFPlus::UI::TextScroller;
5420
5421our @ISA = CFPlus::UI::HBox::;
5422
5423use CFPlus::OpenGL;
1508 5424
1509sub new { 5425sub new {
1510 my $class = shift; 5426 my $class = shift;
1511 5427
1512 my $self = $class->SUPER::new ( 5428 my $self = $class->SUPER::new (
1513 req_w => $::WIDTH / 6, 5429 fontsize => 1,
1514 req_h => $::HEIGHT / 6, 5430 can_events => 1,
1515 fontsize => $::FONTSIZE, 5431 indent => 0,
5432 #font => default_font
1516 @_, 5433 @_,
1517 5434
1518 layout => (new CFClient::Layout), 5435 layout => (new CFPlus::Layout),
1519 par => [], 5436 par => [],
5437 max_par => 0,
1520 height => 0, 5438 height => 0,
1521 children => [ 5439 children => [
1522 (new CFClient::UI::Empty expand => 1), 5440 (new CFPlus::UI::Empty expand => 1),
1523 (new CFClient::UI::Slider vertical => 1), 5441 (new CFPlus::UI::Slider vertical => 1),
1524 ], 5442 ],
1525 ); 5443 );
1526 5444
1527 $self->{children}[1]->connect (changed => sub { 5445 $self->{children}[1]->connect (changed => sub { $self->update });
1528 $self->update;
1529 });
1530 5446
1531 $self 5447 $self
1532} 5448}
1533 5449
1534sub set_fontsize { 5450sub set_fontsize {
1536 5452
1537 $self->{fontsize} = $fontsize; 5453 $self->{fontsize} = $fontsize;
1538 $self->reflow; 5454 $self->reflow;
1539} 5455}
1540 5456
1541sub text_height { 5457sub size_request {
5458 my ($self) = @_;
5459
5460 my ($empty, $slider) = @{ $self->{children} };
5461
5462 local $self->{children} = [$empty, $slider];
5463 $self->SUPER::size_request
5464}
5465
5466sub invoke_size_allocate {
5467 my ($self, $w, $h) = @_;
5468
5469 my ($empty, $slider, @other) = @{ $self->{children} };
5470 $_->configure (@$_{qw(x y req_w req_h)}) for @other;
5471
5472 $self->{layout}->set_font ($self->{font}) if $self->{font};
5473 $self->{layout}->set_height ($self->{fontsize} * $::FONTSIZE);
5474 $self->{layout}->set_width ($empty->{w});
5475 $self->{layout}->set_indent ($self->{fontsize} * $::FONTSIZE * $self->{indent});
5476
5477 $self->reflow;
5478
5479 local $self->{children} = [$empty, $slider];
5480 $self->SUPER::invoke_size_allocate ($w, $h)
5481}
5482
5483sub invoke_mouse_wheel {
1542 my ($self, $text) = @_; 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
5493sub get_layout {
5494 my ($self, $para) = @_;
1543 5495
1544 my $layout = $self->{layout}; 5496 my $layout = $self->{layout};
1545 5497
5498 $layout->set_font ($self->{font}) if $self->{font};
5499 $layout->set_foreground (@{$para->{fg}});
1546 $layout->set_height ($self->{fontsize}); 5500 $layout->set_height ($self->{fontsize} * $::FONTSIZE);
1547 $layout->set_width ($self->{w}); 5501 $layout->set_width ($self->{children}[0]{w} - $para->{indent});
1548 $layout->set_text ($text); 5502 $layout->set_indent ($self->{fontsize} * $::FONTSIZE * $self->{indent});
5503 $layout->set_markup ($para->{markup});
5504
5505 $layout->set_shapes (
5506 map
5507 +(0, $_->baseline_shift +$_->{padding_y} - $_->{h}, $_->{w}, $_->{h}),
5508 @{$para->{widget}}
1549 5509 );
1550 ($layout->size)[1] 5510
5511 $layout
1551} 5512}
1552 5513
1553sub reflow { 5514sub reflow {
1554 my ($self) = @_; 5515 my ($self) = @_;
1555 5516
1556 $self->{need_reflow}++; 5517 $self->{need_reflow}++;
1557 $self->update; 5518 $self->update;
1558} 5519}
1559 5520
1560sub size_request { 5521sub 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
5528sub current_paragraph {
5529 my ($self) = @_;
5530
5531 $self->{top_paragraph} - 1
5532}
5533
5534sub scroll_to {
1561 my ($self) = @_; 5535 my ($self, $para) = @_;
1562 5536
1563 ($self->{req_w}, $self->{req_h}) 5537 $para = List::Util::max 0, List::Util::min $#{$self->{par}}, $para;
1564}
1565 5538
1566sub size_allocate { 5539 $self->{scroll_to} = $para;
1567 my ($self, $x, $y, $w, $h) = @_; 5540 $self->update;
5541}
1568 5542
1569 $self->SUPER::size_allocate ($x, $y, $w, $h); 5543sub clear {
5544 my ($self) = @_;
1570 5545
1571 $self->{layout}->set_height ($self->{fontsize}); 5546 my (undef, undef, @other) = @{ $self->{children} };
1572 $self->{layout}->set_width ($self->{w}); 5547 $self->remove ($_) for @other;
1573 5548
1574 $self->reflow; 5549 $self->{par} = [];
5550 $self->{height} = 0;
5551 $self->{children}[1]->set_range ([0, 0, 0, 1, 1]);
1575} 5552}
1576 5553
1577sub add_paragraph { 5554sub add_paragraph {
1578 my ($self, $color, $text) = @_; 5555 my $self = shift;
1579 5556
1580 #TODO: intelligently "reformat" paragraph 5557 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 };
1581 5567
1582 my $height = $self->text_height ($text); 5568 $self->add (@{ $para->{widget} }) if @{ $para->{widget} };
5569 push @{$self->{par}}, $para;
5570 }
1583 5571
5572 if (my $max = $self->{max_par}) {
5573 shift @{$self->{par}} while @{$self->{par}} > $max;
5574 }
5575
5576 $self->{need_reflow}++;
5577 $self->update;
5578}
5579
5580sub scroll_to_bottom {
5581 my ($self) = @_;
5582
5583 $self->{scroll_to} = $#{$self->{par}};
5584 $self->update;
5585}
5586
5587sub force_uptodate {
5588 my ($self) = @_;
5589
5590 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
1584 $self->{height} += $height; 5609 $self->{height} = $height;
5610 $self->{children}[1]->set_range ([$self->{children}[1]{range}[0], 0, $height, $H, 1]);
1585 5611
1586 push @{$self->{par}}, [$height, $color, $text]; 5612 delete $self->{texture};
5613 }
1587 5614
1588 $self->{children}[1]{range} = [$self->{height} - $self->{h}, 0, $self->{height}, $self->{h}]; 5615 if (my $paridx = delete $self->{scroll_to}) {
1589 $self->{children}[1]->update; 5616 $self->{children}[1]->set_value ($self->{par}[$paridx]{y});
5617 }
1590} 5618}
1591 5619
1592sub update { 5620sub update {
1593 my ($self) = @_; 5621 my ($self) = @_;
1594 5622
1596 5624
1597 return unless $self->{h} > 0; 5625 return unless $self->{h} > 0;
1598 5626
1599 delete $self->{texture}; 5627 delete $self->{texture};
1600 5628
1601 $ROOT->on_refresh ($self, sub { 5629 $ROOT->on_post_alloc ($self => sub {
1602 if (delete $self->{need_reflow}) { 5630 $self->force_uptodate;
1603 my $height = 0;
1604 5631
1605 $height += $_->[0] = $self->text_height ($_->[2]) 5632 my ($W, $H) = @{$self->{children}[0]}{qw(w h)};
1606 for @{$self->{par}};
1607 5633
1608 $self->{height} = $height;
1609
1610 $self->{children}[1]{range} = [$height - $self->{h}, 0, $height, $self->{h}];
1611
1612 delete $self->{texture};
1613 }
1614
1615 $self->{texture} ||= new_from_opengl CFClient::Texture $self->{w}, $self->{h}, sub { 5634 $self->{texture} ||= new_from_opengl CFPlus::Texture $W, $H, sub {
1616 glClearColor 0, 0, 0, 1; 5635 glClearColor 0, 0, 0, 0;
1617 glClear GL_COLOR_BUFFER_BIT; 5636 glClear GL_COLOR_BUFFER_BIT;
1618 5637
1619 glEnable GL_BLEND; 5638 package CFPlus::UI::Base;
1620 glBlendFunc GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA; 5639 local ($draw_x, $draw_y, $draw_w, $draw_h) =
1621 glEnable GL_TEXTURE_2D; 5640 (0, 0, $self->{w}, $self->{h});
1622 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
1623 5641
1624 my $top = int $self->{children}[1]{range}[0]; 5642 my $top = int $self->{children}[1]{range}[0];
1625 5643
5644 my $paridx = 0;
5645 my $top_paragraph;
5646 my $top = int $self->{children}[1]{range}[0];
5647
1626 my $y0 = $top; 5648 my $y0 = $top;
1627 my $y1 = $top + $self->{h}; 5649 my $y1 = $top + $H;
1628 5650
1629 my $y = 0;
1630
1631 my $layout = $self->{layout};
1632
1633 for my $par (@{$self->{par}}) { 5651 for my $para (@{$self->{par}}) {
1634 my $h = $par->[0]; 5652 my $h = $para->{h};
5653 my $y = $para->{y};
1635 5654
1636 if ($y0 < $y + $h && $y < $y1) { 5655 if ($y0 < $y + $h && $y < $y1) {
1637 $layout->set_text ($par->[2]); 5656 my $layout = $self->get_layout ($para);
1638 5657
1639 glColor @{ $par->[1] }; 5658 $layout->render ($para->{indent}, $y - $y0);
1640 my ($W, $H) = $layout->size; 5659
1641 CFClient::Texture->new_from_layout ($layout)->draw_quad (0, $y - $y0); 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 }
1642 } 5672 }
1643 5673
1644 $y += $h; 5674 $paridx++;
5675 $top_paragraph ||= $paridx if $y >= $top;
1645 } 5676 }
1646 5677
1647 glDisable GL_TEXTURE_2D; 5678 $self->{top_paragraph} = $top_paragraph;
1648 glDisable GL_BLEND;
1649 }; 5679 };
1650 }); 5680 });
1651} 5681}
1652 5682
5683sub reconfigure {
5684 my ($self) = @_;
5685
5686 $self->SUPER::reconfigure;
5687
5688 $_->{w} = 1e10 for @{ $self->{par} };
5689 $self->reflow;
5690}
5691
1653sub _draw { 5692sub _draw {
1654 my ($self) = @_; 5693 my ($self) = @_;
1655 5694
1656 if ($self->{texture}) {
1657 glEnable GL_TEXTURE_2D; 5695 glEnable GL_TEXTURE_2D;
1658 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE; 5696 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
1659 $self->{texture}->draw_quad (0, 0, $self->{w}, $self->{h}); 5697 glColor 0, 0, 0, 1;
5698 $self->{texture}->draw_quad_alpha_premultiplied (0, 0, $self->{children}[0]{w}, $self->{children}[0]{h});
1660 glDisable GL_TEXTURE_2D; 5699 glDisable GL_TEXTURE_2D;
1661 }
1662 5700
1663 $self->{children}[1]->draw; 5701 $self->{children}[1]->draw;
1664
1665} 5702}
1666 5703
1667############################################################################# 5704#############################################################################
1668 5705
1669package CFClient::UI::MapWidget;
1670
1671use strict;
1672
1673use List::Util qw(min max);
1674
1675use SDL;
1676use SDL::OpenGL;
1677
1678our @ISA = CFClient::UI::Base::;
1679
1680sub new {
1681 my $class = shift;
1682
1683 $class->SUPER::new (
1684 z => -1,
1685 can_focus => 1,
1686 list => (glGenLists 1),
1687 @_
1688 )
1689}
1690
1691sub key_down {
1692 print "MAPKEYDOWN\n";
1693}
1694
1695sub key_up {
1696}
1697
1698sub button_down {
1699 my ($self, $ev, $x, $y) = @_;
1700
1701 $self->focus_in;
1702
1703 if ($ev->button == 2) {
1704 my ($ox, $oy) = ($ev->button_x, $ev->button_y);
1705 my ($bw, $bh) = ($::CFG->{map_shift_x}, $::CFG->{map_shift_y});
1706
1707 $self->{motion} = sub {
1708 my ($ev, $x, $y) = @_;
1709
1710 ($x, $y) = ($ev->motion_x, $ev->motion_y);
1711
1712 $::CFG->{map_shift_x} = $bw + $x - $ox;
1713 $::CFG->{map_shift_y} = $bh + $y - $oy;
1714
1715 $self->update;
1716 };
1717 }
1718}
1719
1720sub button_up {
1721 my ($self, $ev, $x, $y) = @_;
1722
1723 delete $self->{motion};
1724}
1725
1726sub mouse_motion {
1727 my ($self, $ev, $x, $y) = @_;
1728
1729 $self->{motion}->($ev, $x, $y) if $self->{motion};
1730}
1731
1732sub size_request {
1733 (
1734 1 + 32 * int $::WIDTH / 32,
1735 1 + 32 * int $::HEIGHT / 32,
1736 )
1737}
1738
1739sub update {
1740 my ($self) = @_;
1741
1742 $self->{need_update} = 1;
1743 $self->SUPER::update;
1744}
1745
1746sub draw {
1747 my ($self) = @_;
1748
1749 if (delete $self->{need_update}) {
1750 glNewList $self->{list}, GL_COMPILE;
1751
1752 if ($::MAP) {
1753 my $sw = int $::WIDTH / 32;
1754 my $sh = int $::HEIGHT / 32;
1755
1756 my $sx = $::CFG->{map_shift_x}; my $sx0 = $sx & 31; $sx = ($sx - $sx0) / 32;
1757 my $sy = $::CFG->{map_shift_y}; my $sy0 = $sy & 31; $sy = ($sy - $sy0) / 32;
1758
1759 glTranslate $sx0 - 32, $sy0 - 32, 0;
1760
1761 my ($w, $h, $data) = $::MAP->draw ($sx, $sy, 0, 0, $sw + 1, $sh + 1);
1762
1763 if ($::CFG->{fow_enable}) {
1764 if ($::CFG->{fow_smooth}) { # smooth fog of war
1765 glConvolutionParameter GL_CONVOLUTION_2D, GL_CONVOLUTION_BORDER_MODE, GL_CONSTANT_BORDER;
1766 glConvolutionFilter2D
1767 GL_CONVOLUTION_2D,
1768 GL_ALPHA,
1769 3, 3,
1770 GL_ALPHA, GL_FLOAT,
1771 pack "f*",
1772 0.1, 0.1, 0.1,
1773 0.1, 0.2, 0.1,
1774 0.1, 0.1, 0.1,
1775 ;
1776 glEnable GL_CONVOLUTION_2D;
1777 }
1778
1779 my $tex = new CFClient::Texture
1780 w => $w,
1781 h => $h,
1782 data => $data,
1783 internalformat => GL_ALPHA,
1784 format => GL_ALPHA;
1785
1786 glDisable GL_CONVOLUTION_2D if $::CFG->{fow_smooth};
1787
1788 glEnable GL_BLEND;
1789 glBlendFunc GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA;
1790 glEnable GL_TEXTURE_2D;
1791 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE;
1792
1793 glColor +($::CFG->{fow_intensity}) x 3, 1;
1794 $tex->draw_quad (0, 0, $w * 32, $h * 32);
1795
1796 glDisable GL_TEXTURE_2D;
1797 glDisable GL_BLEND;
1798 }
1799
1800 # HACK BEGIN
1801 {
1802 glTranslate -($sx0 - 32), -($sy0 - 32), 0;#remove
1803 my ($w, $h) = (250, 250);
1804
1805 glEnable GL_BLEND;
1806 glBlendFunc GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA;
1807 glEnable GL_TEXTURE_2D;
1808 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
1809
1810 CFClient::Texture->new (
1811 w => $w,
1812 h => $h,
1813 data => $::MAP->mapmap ($w, $h),
1814 type => GL_UNSIGNED_INT_8_8_8_8_REV
1815 )->draw_quad (100, 100);
1816
1817 glDisable GL_TEXTURE_2D;
1818 glDisable GL_BLEND;
1819 }
1820 # HACK END
1821 }
1822
1823 glEndList;
1824 }
1825
1826 glPushMatrix;
1827 glCallList $self->{list};
1828 glPopMatrix;
1829
1830 if ($FOCUS != $self) {
1831 glColor 64/255, 64/255, 64/255;
1832 glLogicOp GL_AND;
1833 glEnable GL_COLOR_LOGIC_OP;
1834 glBegin GL_QUADS;
1835 glVertex 0, 0;
1836 glVertex 0, $::HEIGHT;
1837 glVertex $::WIDTH, $::HEIGHT;
1838 glVertex $::WIDTH, 0;
1839 glEnd;
1840 glDisable GL_COLOR_LOGIC_OP;
1841 }
1842}
1843
1844my %DIR = (
1845 SDLK_KP8, [1, "north"],
1846 SDLK_KP9, [2, "northeast"],
1847 SDLK_KP6, [3, "east"],
1848 SDLK_KP3, [4, "southeast"],
1849 SDLK_KP2, [5, "south"],
1850 SDLK_KP1, [6, "southwest"],
1851 SDLK_KP4, [7, "west"],
1852 SDLK_KP7, [8, "northwest"],
1853
1854 SDLK_UP, [1, "north"],
1855 SDLK_RIGHT, [3, "east"],
1856 SDLK_DOWN, [5, "south"],
1857 SDLK_LEFT, [7, "west"],
1858);
1859
1860sub key_down {
1861 my ($self, $ev) = @_;
1862
1863 my $mod = $ev->key_mod;
1864 my $sym = $ev->key_sym;
1865
1866 if ($sym == SDLK_KP5) {
1867 $::CONN->user_send ("command stay fire");
1868 } elsif ($sym == SDLK_a) {
1869 $::CONN->user_send ("command apply");
1870 } elsif ($sym == SDLK_QUOTE) {
1871 $self->emit ('activate_console');
1872 } elsif ($sym == SDLK_SLASH) {
1873 $self->emit ('activate_console' => '/');
1874 } elsif (exists $DIR{$sym}) {
1875 if ($mod & KMOD_SHIFT) {
1876 $self->{shft}++;
1877 $::CONN->user_send ("command fire $DIR{$sym}[0]");
1878 } elsif ($mod & KMOD_CTRL) {
1879 $self->{ctrl}++;
1880 $::CONN->user_send ("command run $DIR{$sym}[0]");
1881 } else {
1882 $::CONN->user_send ("command $DIR{$sym}[1]");
1883 }
1884 }
1885}
1886
1887sub key_up {
1888 my ($self, $ev) = @_;
1889
1890 my $mod = $ev->key_mod;
1891 my $sym = $ev->key_sym;
1892
1893 if (!($mod & KMOD_SHIFT) && delete $self->{shft}) {
1894 $::CONN->user_send ("command fire_stop");
1895 }
1896 if (!($mod & KMOD_CTRL ) && delete $self->{ctrl}) {
1897 $::CONN->user_send ("command run_stop");
1898 }
1899}
1900
1901#############################################################################
1902
1903package CFClient::UI::Animator; 5706package CFPlus::UI::Animator;
1904 5707
1905use SDL::OpenGL; 5708use CFPlus::OpenGL;
1906 5709
1907our @ISA = CFClient::UI::Bin::; 5710our @ISA = CFPlus::UI::Bin::;
1908 5711
1909sub moveto { 5712sub moveto {
1910 my ($self, $x, $y) = @_; 5713 my ($self, $x, $y) = @_;
1911 5714
1912 $self->{moveto} = [$self->{x}, $self->{y}, $x, $y]; 5715 $self->{moveto} = [$self->{x}, $self->{y}, $x, $y];
1940 glPopMatrix; 5743 glPopMatrix;
1941} 5744}
1942 5745
1943############################################################################# 5746#############################################################################
1944 5747
1945package CFClient::UI::Flopper; 5748package CFPlus::UI::Flopper;
1946 5749
1947our @ISA = CFClient::UI::Button::; 5750our @ISA = CFPlus::UI::Button::;
1948 5751
1949sub new { 5752sub new {
1950 my $class = shift; 5753 my $class = shift;
1951 5754
1952 my $self = $class->SUPER::new ( 5755 my $self = $class->SUPER::new (
1953 state => 0, 5756 state => 0,
1954 connect_activate => \&toggle_flopper, 5757 on_activate => \&toggle_flopper,
1955 @_ 5758 @_
1956 ); 5759 );
1957 5760
1958 if ($self->{state}) {
1959 $self->{state} = 0;
1960 $self->toggle_flopper;
1961 }
1962
1963 $self 5761 $self
1964} 5762}
1965 5763
1966sub toggle_flopper { 5764sub toggle_flopper {
1967 my ($self) = @_; 5765 my ($self) = @_;
1968 5766
1969 # TODO: use animation 5767 $self->{other}->toggle_visibility;
1970 if ($self->{state} = !$self->{state}) { 5768}
1971 $CFClient::UI::ROOT->add ($self->{other}); 5769
1972 $self->{other}->move ($self->coord2global (0, $self->{h})); 5770#############################################################################
1973 $self->emit ("open"); 5771
5772package CFPlus::UI::Tooltip;
5773
5774our @ISA = CFPlus::UI::Bin::;
5775
5776use CFPlus::OpenGL;
5777
5778sub new {
5779 my $class = shift;
5780
5781 $class->SUPER::new (
5782 @_,
5783 can_events => 0,
5784 )
5785}
5786
5787sub set_tooltip_from {
5788 my ($self, $widget) = @_;
5789
5790 $widget->{tooltip} = CFPlus::Pod::section_label tooltip => $1
5791 if $widget->{tooltip} =~ /^#(.*)$/;
5792
5793 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 $tooltip =~ s/^\n+//;
5803 $tooltip =~ s/\n+$//;
5804
5805 $self->add (new CFPlus::UI::Label
5806 markup => $tooltip,
5807 max_w => ($widget->{tooltip_width} || 0.25) * $::WIDTH,
5808 fontsize => 0.8,
5809 style => 1, # FLAG_INVERSE
5810 ellipsise => 0,
5811 font => ($widget->{tooltip_font} || $::FONT_PROP),
5812 );
5813}
5814
5815sub size_request {
5816 my ($self) = @_;
5817
5818 my ($w, $h) = @{$self->child}{qw(req_w req_h)};
5819
5820 ($w + 4, $h + 4)
5821}
5822
5823sub invoke_size_allocate {
5824 my ($self, $w, $h) = @_;
5825
5826 $self->SUPER::invoke_size_allocate ($w - 4, $h - 4)
5827}
5828
5829sub invoke_visibility_change {
5830 my ($self, $visible) = @_;
5831
5832 return unless $visible;
5833
5834 $self->{root}->on_post_alloc ("move_$self" => sub {
5835 my $widget = $self->{owner}
5836 or return;
5837
5838 if ($widget->{visible}) {
5839 my ($x, $y) = $widget->coord2global ($widget->{w}, 0);
5840
5841 ($x, $y) = $widget->coord2global (-$self->{w}, 0)
5842 if $x + $self->{w} > $self->{root}{w};
5843
5844 $self->move_abs ($x, $y);
5845 } else {
5846 $self->hide;
5847 }
5848 });
5849}
5850
5851sub _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 glTranslate 2 - 0.375, 2 - 0.375;
5875
5876 $self->SUPER::_draw;
5877}
5878
5879#############################################################################
5880
5881package CFPlus::UI::Face;
5882
5883our @ISA = CFPlus::UI::DrawBG::;
5884
5885use CFPlus::OpenGL;
5886
5887sub new {
5888 my $class = shift;
5889
5890 my $self = $class->SUPER::new (
5891 size_w => 32,
5892 size_h => 8,
5893 aspect => 1,
5894 can_events => 0,
5895 @_,
5896 );
5897
5898 if ($self->{anim} && $self->{animspeed}) {
5899 CFPlus::weaken (my $widget = $self);
5900
5901 $widget->{animspeed} = List::Util::max 0.05, $widget->{animspeed};
5902 $widget->{anim_start} = $self->{animspeed} * Event::time / $self->{animspeed};
5903 $self->{timer} = Event->timer (
5904 parked => 1,
5905 cb => sub {
5906 return unless $::CONN && $widget;
5907
5908 ++$widget->{frame};
5909 $widget->update_face;
5910 $widget->update;
5911
5912 $widget->update_timer;
5913 },
5914 );
5915
5916 $self->update_face;
5917 $self->update_timer;
5918 }
5919
5920 $self
5921}
5922
5923sub 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;
1974 } else { 5935 } else {
1975 $CFClient::UI::ROOT->remove ($self->{other}); 5936 $self->{timer}->stop;
1976 $self->emit ("close"); 5937 }
5938}
5939
5940sub 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
5953sub size_request {
5954 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}
5970
5971sub update {
5972 my ($self) = @_;
5973
5974 return unless $self->{visible};
5975
5976 $self->SUPER::update;
5977}
5978
5979sub invoke_visibility_change {
5980 my ($self) = @_;
5981
5982 $self->update_timer;
5983
5984 0
5985}
5986
5987sub _draw {
5988 my ($self) = @_;
5989
5990 return unless $::CONN;
5991
5992 $self->SUPER::_draw;
5993
5994 my $faceid = $::CONN->{faceid}[$self->{face}]
5995 or return;
1977 } 5996
5997 my $tex = $::CONN->{texture}[$faceid];
1978 5998
1979 $self->emit (changed => $self->{state}); 5999 if ($tex) {
6000 glEnable GL_TEXTURE_2D;
6001 glTexEnv GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE;
6002 glColor 0, 0, 0, 1;
6003 $tex->draw_quad_alpha (0, 0, $self->{w}, $self->{h});
6004 glDisable GL_TEXTURE_2D;
6005 }
6006}
6007
6008sub destroy {
6009 my ($self) = @_;
6010
6011 (delete $self->{timer})->cancel
6012 if $self->{timer};
6013
6014 $self->SUPER::destroy;
1980} 6015}
1981 6016
1982############################################################################# 6017#############################################################################
1983 6018
1984package CFClient::UI::Root; 6019package CFPlus::UI::Buttonbar;
1985 6020
6021our @ISA = CFPlus::UI::HBox::;
6022
6023# TODO: should actually wrap buttons and other goodies.
6024
6025#############################################################################
6026
6027package CFPlus::UI::Menu;
6028
6029our @ISA = CFPlus::UI::Toplevel::;
6030
6031use CFPlus::OpenGL;
6032
6033sub new {
6034 my $class = shift;
6035
6036 my $self = $class->SUPER::new (
6037 items => [],
6038 z => 100,
6039 @_,
6040 );
6041
6042 $self->add ($self->{vbox} = new CFPlus::UI::VBox);
6043
6044 for my $item (@{ $self->{items} }) {
6045 my ($widget, $cb, $tooltip) = @$item;
6046
6047 # handle various types of items, only text for now
6048 if (!ref $widget) {
6049 if ($widget =~ /\t/) {
6050 my ($left, $right) = split /\t/, $widget, 2;
6051
6052 $widget = new CFPlus::UI::HBox
6053 can_hover => 1,
6054 can_events => 1,
6055 tooltip => $tooltip,
6056 children => [
6057 (new CFPlus::UI::Label markup => $left, expand => 1),
6058 (new CFPlus::UI::Label markup => $right, align => +1),
6059 ],
6060 ;
6061
6062 } else {
6063 $widget = new CFPlus::UI::Label
6064 can_hover => 1,
6065 can_events => 1,
6066 markup => $widget,
6067 tooltip => $tooltip;
6068 }
6069 }
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)
6080sub popup {
6081 my ($self, $ev) = @_;
6082
6083 $self->emit ("popdown");
6084
6085 # maybe save $GRAB? must be careful about events...
6086 $GRAB = $self;
6087 $self->{button} = $ev->{button};
6088
6089 $self->show;
6090 $self->move_abs ($ev->{x} - $self->{w} * 0.5, $ev->{y} - $self->{h} * 0.5);
6091}
6092
6093sub invoke_mouse_motion {
6094 my ($self, $ev, $x, $y) = @_;
6095
6096 # TODO: should use vbox->find_widget or so
6097 $HOVER = $ROOT->find_widget ($ev->{x}, $ev->{y});
6098 $self->{hover} = $self->{item}{$HOVER};
6099
6100 0
6101}
6102
6103sub invoke_button_up {
6104 my ($self, $ev, $x, $y) = @_;
6105
6106 if ($ev->{button} == $self->{button}) {
6107 undef $GRAB;
6108 $self->hide;
6109
6110 $self->emit ("popdown");
6111 $self->{hover}[1]->() if $self->{hover};
6112 } else {
6113 return 0
6114 }
6115
6116 1
6117}
6118
6119#############################################################################
6120
6121package CFPlus::UI::Multiplexer;
6122
1986our @ISA = CFClient::UI::Container::; 6123our @ISA = CFPlus::UI::Container::;
1987 6124
1988use SDL::OpenGL; 6125sub 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
6138sub add {
6139 my ($self, @widgets) = @_;
6140
6141 $self->SUPER::add (@widgets);
6142
6143 $self->{current} = $self->{children}[0]
6144 if @{ $self->{children} };
6145}
6146
6147sub get_current_page {
6148 my ($self) = @_;
6149
6150 $self->{current}
6151}
6152
6153sub 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 $self->emit (page_changed => $self->{current});
6164
6165 $self->realloc;
6166}
6167
6168sub visible_children {
6169 $_[0]{current}
6170}
1989 6171
1990sub size_request { 6172sub size_request {
1991 ($::WIDTH, $::HEIGHT) 6173 my ($self) = @_;
1992}
1993 6174
6175 $self->{current}->size_request
6176}
6177
1994sub size_allocate { 6178sub invoke_size_allocate {
1995 my ($self, $x, $y, $w, $h) = @_; 6179 my ($self, $w, $h) = @_;
1996 6180
1997 $self->_size_allocate ($x, $y, $w, $h); 6181 $self->{current}->configure (0, 0, $w, $h);
1998 6182
1999 $_->size_allocate ($_->{x}, $_->{y}, $_->size_request) 6183 1
2000 for @{$self->{children}};
2001} 6184}
2002 6185
6186sub _draw {
6187 my ($self) = @_;
6188
6189 $self->{current}->draw;
6190}
6191
6192#############################################################################
6193
6194package CFPlus::UI::Notebook;
6195
6196our @ISA = CFPlus::UI::VBox::;
6197
6198sub new {
6199 my $class = shift;
6200
6201 my $self = $class->SUPER::new (
6202 buttonbar => (new CFPlus::UI::Buttonbar),
6203 multiplexer => (new CFPlus::UI::Multiplexer expand => 1),
6204 # filter => # will be put between multiplexer and $self
6205 @_,
6206 );
6207
6208 $self->{filter}->add ($self->{multiplexer}) if $self->{filter};
6209 $self->SUPER::add ($self->{buttonbar}, $self->{filter} || $self->{multiplexer});
6210
6211 $self
6212}
6213
6214sub add {
6215 my ($self, $title, $widget, $tooltip) = @_;
6216
6217 CFPlus::weaken $self;
6218
6219 $self->{buttonbar}->add (new CFPlus::UI::Button
6220 markup => $title,
6221 tooltip => $tooltip,
6222 on_activate => sub { $self->set_current_page ($widget) },
6223 );
6224
6225 $self->{multiplexer}->add ($widget);
6226}
6227
6228sub get_current_page {
6229 my ($self) = @_;
6230
6231 $self->{multiplexer}->get_current_page
6232}
6233
6234sub set_current_page {
6235 my ($self, $page) = @_;
6236
6237 $self->{multiplexer}->set_current_page ($page);
6238 $self->emit (page_changed => $self->{multiplexer}{current});
6239}
6240
6241#############################################################################
6242
6243package CFPlus::UI::Selector;
6244
6245use utf8;
6246
6247our @ISA = CFPlus::UI::Button::;
6248
6249sub new {
6250 my $class = shift;
6251
6252 my $self = $class->SUPER::new (
6253 options => [], # [value, title, longdesc], ...
6254 value => undef,
6255 @_,
6256 );
6257
6258 $self->_set_value ($self->{value});
6259
6260 $self
6261}
6262
6263sub invoke_button_down {
6264 my ($self, $ev) = @_;
6265
6266 my @menu_items;
6267
6268 for (@{ $self->{options} }) {
6269 my ($value, $title, $tooltip) = @$_;
6270
6271 push @menu_items, [$tooltip || $title, sub { $self->set_value ($value) }];
6272 }
6273
6274 CFPlus::UI::Menu->new (items => \@menu_items)->popup ($ev);
6275}
6276
6277sub _set_value {
6278 my ($self, $value) = @_;
6279
6280 my ($item) = grep $_->[0] eq $value, @{ $self->{options} }
6281 or return;
6282
6283 $self->{value} = $item->[0];
6284 $self->set_markup ("$item->[1] ⇓");
6285 $self->set_tooltip ($item->[2]);
6286}
6287
6288sub set_value {
6289 my ($self, $value) = @_;
6290
6291 return unless $self->{value} ne $value;
6292
6293 $self->_set_value ($value);
6294 $self->emit (changed => $value);
6295}
6296
6297#############################################################################
6298
6299package CFPlus::UI::Statusbox;
6300
6301our @ISA = CFPlus::UI::VBox::;
6302
6303sub new {
6304 my $class = shift;
6305
6306 my $self = $class->SUPER::new (
6307 fontsize => 0.8,
6308 @_,
6309 );
6310
6311 CFPlus::weaken (my $this = $self);
6312
6313 $self->{timer} = Event->timer (after => 1, interval => 1, cb => sub { $this->reorder });
6314
6315 $self
6316}
6317
6318sub reorder {
6319 my ($self) = @_;
6320 my $NOW = Time::HiRes::time;
6321
6322 # freeze display when hovering over any label
6323 return if $CFPlus::UI::TOOLTIP->{owner}
6324 && grep $CFPlus::UI::TOOLTIP->{owner} == $_->{label},
6325 values %{ $self->{item} };
6326
6327 while (my ($k, $v) = each %{ $self->{item} }) {
6328 delete $self->{item}{$k} if $v->{timeout} < $NOW;
6329 }
6330
6331 my @widgets;
6332
6333 my @items = sort {
6334 $a->{pri} <=> $b->{pri}
6335 or $b->{id} <=> $a->{id}
6336 } values %{ $self->{item} };
6337
6338 $self->{timer}->interval (1);
6339
6340 my $count = 10 + 1;
6341 for my $item (@items) {
6342 last unless --$count;
6343
6344 my $label = $item->{label} ||= do {
6345 # TODO: doesn't handle markup well (read as: at all)
6346 my $short = $item->{count} > 1
6347 ? "<b>$item->{count} ×</b> $item->{text}"
6348 : $item->{text};
6349
6350 for ($short) {
6351 s/^\s+//;
6352 s/\s+/ /g;
6353 }
6354
6355 new CFPlus::UI::Label
6356 markup => $short,
6357 tooltip => $item->{tooltip},
6358 tooltip_font => $::FONT_PROP,
6359 tooltip_width => 0.67,
6360 fontsize => $item->{fontsize} || $self->{fontsize},
6361 max_w => $::WIDTH * 0.44,
6362 fg => [@{ $item->{fg} }],
6363 can_events => 1,
6364 can_hover => 1
6365 };
6366
6367 if ((my $diff = $item->{timeout} - $NOW) < 2) {
6368 $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 $self->{timer}->interval (1/30);
6373 } else {
6374 $label->{fg}[3] = $item->{fg}[3] || 1;
6375 }
6376
6377 push @widgets, $label;
6378 }
6379
6380 $self->clear;
6381 $self->SUPER::add (reverse @widgets);
6382}
6383
6384sub add {
6385 my ($self, $text, %arg) = @_;
6386
6387 $text =~ s/^\s+//;
6388 $text =~ s/\s+$//;
6389
6390 return unless $text;
6391
6392 my $timeout = (int time) + ((delete $arg{timeout}) || 60);
6393
6394 my $group = exists $arg{group} ? $arg{group} : ++$self->{id};
6395
6396 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 $item->{id} += 0.2;#d#
6404 $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 fg => [0.8, 0.8, 0.8, 0.8],
6413 pri => 0,
6414 count => 1,
6415 %arg,
6416 };
6417 }
6418
6419 $ROOT->on_refresh (reorder => sub {
6420 $self->reorder;
6421 });
6422}
6423
6424sub reconfigure {
6425 my ($self) = @_;
6426
6427 delete $_->{label}
6428 for values %{ $self->{item} || {} };
6429
6430 $self->reorder;
6431 $self->SUPER::reconfigure;
6432}
6433
6434sub destroy {
6435 my ($self) = @_;
6436
6437 $self->{timer}->cancel;
6438
6439 $self->SUPER::destroy;
6440}
6441
6442#############################################################################
6443
6444package CFPlus::UI::Root;
6445
6446our @ISA = CFPlus::UI::Container::;
6447
6448use List::Util qw(min max);
6449
6450use CFPlus::OpenGL;
6451
6452sub new {
6453 my $class = shift;
6454
6455 my $self = $class->SUPER::new (
6456 visible => 1,
6457 @_,
6458 );
6459
6460 CFPlus::weaken ($self->{root} = $self);
6461
6462 $self
6463}
6464
6465sub size_request {
6466 my ($self) = @_;
6467
6468 ($self->{w}, $self->{h})
6469}
6470
2003sub _topleft { 6471sub _to_pixel {
6472 my ($coord, $size, $max) = @_;
6473
6474 $coord =
6475 $coord eq "center" ? ($max - $size) * 0.5
6476 : $coord eq "max" ? $max
6477 : $coord;
6478
6479 $coord = 0 if $coord < 0;
6480 $coord = $max - $size if $coord > $max - $size;
6481
6482 int $coord + 0.5
6483}
6484
6485sub invoke_size_allocate {
6486 my ($self, $w, $h) = @_;
6487
6488 for my $child ($self->children) {
6489 my ($X, $Y, $W, $H) = @$child{qw(x y req_w req_h)};
6490
6491 $X = $child->{force_x} if exists $child->{force_x};
6492 $Y = $child->{force_y} if exists $child->{force_y};
6493
6494 $X = _to_pixel $X, $W, $self->{w};
6495 $Y = _to_pixel $Y, $H, $self->{h};
6496
6497 $child->configure ($X, $Y, $W, $H);
6498 }
6499
6500 1
6501}
6502
6503sub coord2local {
2004 my ($self, $x, $y) = @_; 6504 my ($self, $x, $y) = @_;
2005 6505
2006 ($x, $y) 6506 ($x, $y)
2007} 6507}
2008 6508
6509sub coord2global {
6510 my ($self, $x, $y) = @_;
6511
6512 ($x, $y)
6513}
6514
2009sub update { 6515sub update {
2010 my ($self) = @_; 6516 my ($self) = @_;
2011 6517
2012 $self->size_allocate (0, 0, $::WIDTH, $::HEIGHT); 6518 $::WANT_REFRESH++;
2013 ::refresh ();
2014} 6519}
2015 6520
2016sub add { 6521sub add {
2017 my ($self, $widget) = @_; 6522 my ($self, @children) = @_;
2018 6523
6524 $_->{is_toplevel} = 1
6525 for @children;
6526
2019 $self->SUPER::add ($widget); 6527 $self->SUPER::add (@children);
6528}
2020 6529
2021 $widget->size_allocate ($widget->{x}, $widget->{y}, $widget->size_request); 6530sub remove {
6531 my ($self, @children) = @_;
6532
6533 $self->SUPER::remove (@children);
6534
6535 delete $self->{is_toplevel}
6536 for @children;
6537
6538 while (@children) {
6539 my $w = pop @children;
6540 push @children, $w->children;
6541 $w->set_invisible;
6542 }
2022} 6543}
2023 6544
2024sub on_refresh { 6545sub on_refresh {
2025 my ($self, $id, $cb) = @_; 6546 my ($self, $id, $cb) = @_;
2026 6547
2027 $self->{refresh_hook}{$id} = $cb; 6548 $self->{refresh_hook}{$id} = $cb;
2028} 6549}
2029 6550
6551sub on_post_alloc {
6552 my ($self, $id, $cb) = @_;
6553
6554 $self->{post_alloc_hook}{$id} = $cb;
6555}
6556
2030sub draw { 6557sub draw {
2031 my ($self) = @_; 6558 my ($self) = @_;
2032 6559
2033 while (my $rcb = delete $self->{refresh_hook}) { 6560 while ($self->{refresh_hook}) {
2034 $_->() for values %$rcb; 6561 $_->()
6562 for values %{delete $self->{refresh_hook}};
6563 }
6564
6565 if ($self->{realloc}) {
6566 my %queue;
6567 my @queue;
6568 my $widget;
6569
6570 outer:
6571 while () {
6572 if (my $realloc = delete $self->{realloc}) {
6573 for $widget (values %$realloc) {
6574 $widget->{visible} or next; # do not resize invisible widgets
6575
6576 $queue{$widget+0}++ and next; # duplicates are common
6577
6578 push @{ $queue[$widget->{visible}] }, $widget;
6579 }
6580 }
6581
6582 while () {
6583 @queue or last outer;
6584
6585 $widget = pop @{ $queue[-1] || [] }
6586 and last;
6587
6588 pop @queue;
6589 }
6590
6591 delete $queue{$widget+0};
6592
6593 my ($w, $h) = $widget->size_request;
6594
6595 $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
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 $self->{realloc}{$parent+0} = $parent
6613 unless $queue{$parent+0};
6614
6615 $parent->{force_size_alloc} = 1;
6616 $self->{size_alloc}{$parent+0} = $parent;
6617 }
6618 }
6619
6620 delete $self->{realloc}{$widget+0};
6621 }
6622 }
6623
6624 while (my $size_alloc = delete $self->{size_alloc}) {
6625 my @queue = sort { $b->{visible} <=> $a->{visible} }
6626 values %$size_alloc;
6627
6628 while () {
6629 my $widget = pop @queue || last;
6630
6631 my ($w, $h) = @$widget{qw(alloc_w alloc_h)};
6632
6633 $w = 0 if $w < 0;
6634 $h = 0 if $h < 0;
6635
6636 $w = max $widget->{min_w}, $w;
6637 $h = max $widget->{min_h}, $h;
6638
6639# $w = min $self->{w} - $widget->{x}, $w if $self->{w};
6640# $h = min $self->{h} - $widget->{y}, $h if $self->{h};
6641
6642 $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 $w = int $w + 0.5;
6646 $h = int $h + 0.5;
6647
6648 if ($widget->{w} != $w || $widget->{h} != $h || delete $widget->{force_size_alloc}) {
6649 $widget->{old_w} = $widget->{w};
6650 $widget->{old_h} = $widget->{h};
6651
6652 $widget->{w} = $w;
6653 $widget->{h} = $h;
6654
6655 $widget->emit (size_allocate => $w, $h);
6656 }
6657 }
6658 }
6659
6660 while ($self->{post_alloc_hook}) {
6661 $_->()
6662 for values %{delete $self->{post_alloc_hook}};
2035 } 6663 }
2036 6664
2037 glViewport 0, 0, $::WIDTH, $::HEIGHT; 6665 glViewport 0, 0, $::WIDTH, $::HEIGHT;
2038 glClearColor +($::CFG->{fow_intensity}) x 3, 1; 6666 glClearColor +($::CFG->{fow_intensity}) x 3, 1;
2039 glClear GL_COLOR_BUFFER_BIT; 6667 glClear GL_COLOR_BUFFER_BIT;
2040 6668
2041 glMatrixMode GL_PROJECTION; 6669 glMatrixMode GL_PROJECTION;
2042 glLoadIdentity; 6670 glLoadIdentity;
2043 glOrtho 0, $::WIDTH, $::HEIGHT, 0, -10000 , 10000; 6671 glOrtho 0, $::WIDTH, $::HEIGHT, 0, -10000, 10000;
2044 glMatrixMode GL_MODELVIEW; 6672 glMatrixMode GL_MODELVIEW;
2045 glLoadIdentity; 6673 glLoadIdentity;
2046 6674
6675 {
6676 package CFPlus::UI::Base;
6677
6678 local ($draw_x, $draw_y, $draw_w, $draw_h) =
6679 (0, 0, $self->{w}, $self->{h});
6680
2047 $self->_draw; 6681 $self->_draw;
6682 }
2048} 6683}
2049 6684
2050############################################################################# 6685#############################################################################
2051 6686
2052package CFClient::UI; 6687package CFPlus::UI;
2053 6688
2054$ROOT = new CFClient::UI::Root; 6689$ROOT = new CFPlus::UI::Root;
6690$TOOLTIP = new CFPlus::UI::Tooltip z => 900;
2055 6691
20561 66921
2057 6693

Diff Legend

Removed lines
+ Added lines
< Changed lines
> Changed lines