1 /**
2 * @file
3 * @brief Hierarchical layout system.
4 **/
5
6 #include "AppHdr.h"
7
8 #include <numeric>
9 #include <stack>
10 #include <chrono>
11 #include <cwctype>
12
13 #include "ui.h"
14 #include "cio.h"
15 #include "macro.h"
16 #include "state.h"
17 #include "tileweb.h"
18 #include "unicode.h"
19 #include "libutil.h"
20 #include "windowmanager.h"
21 #include "ui-scissor.h"
22
23 #ifdef USE_TILE_LOCAL
24 # include "glwrapper.h"
25 # include "tilebuf.h"
26 # include "tilepick-p.h"
27 # include "tile-player-flag-cut.h"
28 #else
29 # if defined(UNIX) || defined(TARGET_COMPILER_MINGW)
30 # include <unistd.h>
31 # endif
32 # include "output.h"
33 # include "stringutil.h"
34 # include "view.h"
35 #endif
36
37 #ifdef USE_TILE_WEB
38 # include <unordered_map>
39 #endif
40
41 namespace ui {
42
43 #ifndef USE_TILE_LOCAL
44 static void clear_text_region(Region region, COLOURS bg);
45 #endif
46
47 static struct UIRoot
48 {
49 public:
50 void push_child(shared_ptr<Widget> child, KeymapContext km);
51 void pop_child();
52
top_childui::UIRoot53 shared_ptr<Widget> top_child()
54 {
55 const auto sz = m_root.num_children();
56 return sz > 0 ? m_root.get_child(sz-1) : nullptr;
57 }
58
num_childrenui::UIRoot59 size_t num_children() const
60 {
61 return m_root.num_children();
62 };
63
widget_is_in_layoutui::UIRoot64 bool widget_is_in_layout(const Widget* w)
65 {
66 for (; w; w = w->_get_parent())
67 if (w == &m_root)
68 return true;
69 return false;
70 }
71
72 void resize(int w, int h);
73 void layout();
74 void render();
75 void swap_buffers();
76
77 bool on_event(wm_event& event);
78 bool deliver_event(Event& event);
79
queue_layoutui::UIRoot80 void queue_layout()
81 {
82 m_needs_layout = true;
83 }
84
expose_regionui::UIRoot85 void expose_region(Region r)
86 {
87 if (r.empty())
88 return;
89 if (m_dirty_region.empty())
90 m_dirty_region = r;
91 else
92 m_dirty_region = m_dirty_region.aabb_union(r);
93 needs_paint = true;
94 }
95
96 bool needs_paint;
97 bool needs_swap;
98
99 #ifdef DEBUG
100 bool debug_draw = false;
101 bool debug_on_event(const wm_event& event);
102 void debug_render();
103 #endif
104
105 struct LayoutInfo
106 {
107 KeymapContext keymap = KMC_NONE;
108 Widget* current_focus = nullptr;
109 Widget* default_focus = nullptr;
110 int generation_id = 0;
111 };
112
113 LayoutInfo state; // current keymap and focus info
114 int next_generation_id = 1;
115
116 vector<int> cutoff_stack;
117 vector<Widget*> default_focus_stack;
118 vector<Widget*> focus_order;
119
120 void update_focus_order();
121 void focus_next();
122 void focus_prev();
123
124 struct RestartAllocation {};
125
126 #ifdef USE_TILE_LOCAL
127 vector<Widget*> hover_path;
128
129 void update_hover_path();
130 void update_hover_path_for_widget(Widget* widget);
131 void send_mouse_enter_leave_events(
132 const vector<Widget*>& old_hover_path,
133 const vector<Widget*>& new_hover_path);
134 #endif
135
136 #ifdef USE_TILE_WEB
137 void update_synced_widgets();
138 unordered_map<string, Widget*> synced_widgets;
139 bool receiving_ui_state = false;
140 void sync_state();
141 void recv_ui_state_change(const JsonNode *state);
142 #endif
143
144 coord_def cursor_pos;
145
146 protected:
147 int m_w, m_h;
148 Region m_region;
149 Region m_dirty_region;
150 Stack m_root;
151 bool m_needs_layout{false};
152 bool m_changed_layout_since_click = false;
153 vector<LayoutInfo> saved_layout_info;
154 } ui_root;
155
156 static ScissorStack scissor_stack;
157
158 struct Widget::slots Widget::slots = {};
159
Event(Event::Type _type)160 Event::Event(Event::Type _type) : m_type(_type)
161 {
162 }
163
KeyEvent(Event::Type _type,const wm_keyboard_event & wm_ev)164 KeyEvent::KeyEvent(Event::Type _type, const wm_keyboard_event& wm_ev) : Event(_type)
165 {
166 m_key = wm_ev.keysym.sym;
167 }
168
169 #ifdef USE_TILE_LOCAL
MouseEvent(Event::Type _type,const wm_mouse_event & wm_ev)170 MouseEvent::MouseEvent(Event::Type _type, const wm_mouse_event& wm_ev) : Event(_type)
171 {
172 m_button = static_cast<MouseEvent::Button>(wm_ev.button);
173 // XXX: is it possible that the cursor has moved since the SDL event fired?
174 wm->get_mouse_state(&m_x, &m_y);
175 m_wheel_dx = _type == MouseWheel ? wm_ev.px : 0;
176 m_wheel_dy = _type == MouseWheel ? wm_ev.py : 0;
177 }
178 #endif
179
FocusEvent(Event::Type type)180 FocusEvent::FocusEvent(Event::Type type) : Event(type)
181 {
182 }
183
ActivateEvent()184 ActivateEvent::ActivateEvent() : Event(Event::Type::Activate)
185 {
186 }
187
~Widget()188 Widget::~Widget()
189 {
190 Widget::slots.event.remove_by_target(this);
191 Widget::slots.hotkey.remove_by_target(this);
192 if (m_parent && get_focused_widget() == this)
193 set_focused_widget(nullptr);
194 _set_parent(nullptr);
195 erase_val(ui_root.focus_order, this);
196 #ifdef USE_TILE_WEB
197 if (!m_sync_id.empty())
198 ui_root.synced_widgets.erase(m_sync_id);
199 #endif
200 }
201
_emit_layout_pop()202 void Widget::_emit_layout_pop()
203 {
204 Widget::slots.layout_pop.emit(this);
205 Widget::slots.layout_pop.remove_by_target(this);
206 }
207
on_event(const Event & event)208 bool Widget::on_event(const Event& event)
209 {
210 return Widget::slots.event.emit(this, event);
211 }
212
allocate_region(Region)213 void OverlayWidget::allocate_region(Region)
214 {
215 // Occupies 0 space, and therefore will never clear the screen.
216 m_region = {0, 0, 0, 0};
217 _allocate_region();
218 }
219
_expose()220 void OverlayWidget::_expose()
221 {
222 // forcibly ensure that renders will be called. This sidesteps the region
223 // based code for deciding whether anything should be rendered, and leaves
224 // it up to the OverlayWidget.
225 ui_root.needs_paint = true;
226 }
227
get_child_at_offset(int x,int y)228 shared_ptr<Widget> ContainerVec::get_child_at_offset(int x, int y)
229 {
230 for (shared_ptr<Widget>& child : m_children)
231 if (child->get_region().contains_point(x, y))
232 return child;
233 return nullptr;
234 }
235
get_child_at_offset(int x,int y)236 shared_ptr<Widget> Bin::get_child_at_offset(int x, int y)
237 {
238 if (m_child && m_child->get_region().contains_point(x, y))
239 return m_child;
240 return nullptr;
241 }
242
set_child(shared_ptr<Widget> child)243 void Bin::set_child(shared_ptr<Widget> child)
244 {
245 child->_set_parent(this);
246 m_child = move(child);
247 _invalidate_sizereq();
248 }
249
render()250 void Widget::render()
251 {
252 if (m_visible)
253 _render();
254 }
255
get_preferred_size(Direction dim,int prosp_width)256 SizeReq Widget::get_preferred_size(Direction dim, int prosp_width)
257 {
258 ASSERT((dim == HORZ) == (prosp_width == -1));
259
260 if (!m_visible)
261 return { 0, 0 };
262
263 if (cached_sr_valid[dim] && (!dim || cached_sr_pw == prosp_width))
264 return cached_sr[dim];
265
266 prosp_width = dim ? prosp_width - margin.right - margin.left : prosp_width;
267 SizeReq ret = _get_preferred_size(dim, prosp_width);
268 ASSERT(ret.min <= ret.nat);
269
270 // Order is important: max sizes limit expansion, and don't include margins
271 const bool expand = dim ? expand_v : expand_h;
272 const bool shrink = dim ? shrink_v : shrink_h;
273 ASSERT(!(expand && shrink));
274 constexpr int ui_expand_sz = 0xffffff;
275
276 if (expand)
277 ret.nat = ui_expand_sz;
278 else if (shrink)
279 ret.nat = ret.min;
280
281 int& _min_size = dim ? m_min_size.height : m_min_size.width;
282 int& _max_size = dim ? m_max_size.height : m_max_size.width;
283
284 ASSERT(_min_size <= _max_size);
285 ret.min = max(ret.min, _min_size);
286 ret.nat = min(ret.nat, max(_max_size, ret.min));
287 ret.nat = max(ret.nat, ret.min);
288 ASSERT(ret.min <= ret.nat);
289
290 const int m = dim ? margin.top + margin.bottom : margin.left + margin.right;
291 ret.min += m;
292 ret.nat += m;
293
294 ret.nat = min(ret.nat, ui_expand_sz);
295
296 cached_sr_valid[dim] = true;
297 cached_sr[dim] = ret;
298 if (dim)
299 cached_sr_pw = prosp_width;
300
301 return ret;
302 }
303
allocate_region(Region region)304 void Widget::allocate_region(Region region)
305 {
306 if (!m_visible)
307 return;
308
309 Region new_region = {
310 region.x + margin.left,
311 region.y + margin.top,
312 region.width - margin.left - margin.right,
313 region.height - margin.top - margin.bottom,
314 };
315
316 if (m_region == new_region && !alloc_queued)
317 return;
318 ui_root.expose_region(m_region);
319 ui_root.expose_region(new_region);
320 m_region = new_region;
321 alloc_queued = false;
322
323 ASSERT(m_region.width >= 0);
324 ASSERT(m_region.height >= 0);
325 _allocate_region();
326 }
327
_get_preferred_size(Direction,int)328 SizeReq Widget::_get_preferred_size(Direction, int)
329 {
330 return { 0, 0xffffff };
331 }
332
_allocate_region()333 void Widget::_allocate_region()
334 {
335 }
336
337 /**
338 * Determine whether a widget contains the given widget.
339 *
340 * @param child The other widget.
341 * @return True if the other widget is a descendant of this widget.
342 */
is_ancestor_of(const shared_ptr<Widget> & other)343 bool Widget::is_ancestor_of(const shared_ptr<Widget>& other)
344 {
345 for (Widget* w = other.get(); w; w = w->_get_parent())
346 if (w == this)
347 return true;
348 return false;
349 }
350
_set_parent(Widget * p)351 void Widget::_set_parent(Widget* p)
352 {
353 m_parent = p;
354 #ifdef USE_TILE_LOCAL
355 ui_root.update_hover_path_for_widget(this);
356 #endif
357 }
358
359 /**
360 * Unparent a widget.
361 *
362 * This function verifies that a widget has the correct parent before orphaning.
363 * Intended for use in container widget destructors.
364 *
365 * @param child The child widget to unparent.
366 */
_unparent(shared_ptr<Widget> & child)367 void Widget::_unparent(shared_ptr<Widget>& child)
368 {
369 if (child->m_parent == this)
370 child->_set_parent(nullptr);
371 }
372
_invalidate_sizereq(bool immediate)373 void Widget::_invalidate_sizereq(bool immediate)
374 {
375 for (auto w = this; w; w = w->m_parent)
376 fill(begin(w->cached_sr_valid), end(w->cached_sr_valid), false);
377 if (immediate)
378 ui_root.queue_layout();
379 }
380
_queue_allocation(bool immediate)381 void Widget::_queue_allocation(bool immediate)
382 {
383 for (auto w = this; w && !w->alloc_queued; w = w->m_parent)
384 w->alloc_queued = true;
385 if (immediate)
386 ui_root.queue_layout();
387 }
388
_expose()389 void Widget::_expose()
390 {
391 ui_root.expose_region(m_region);
392 }
393
set_visible(bool visible)394 void Widget::set_visible(bool visible)
395 {
396 if (m_visible == visible)
397 return;
398 m_visible = visible;
399 _invalidate_sizereq();
400 }
401
add_internal_child(shared_ptr<Widget> child)402 void Widget::add_internal_child(shared_ptr<Widget> child)
403 {
404 child->_set_parent(this);
405 m_internal_children.emplace_back(move(child));
406 }
407
set_sync_id(string id)408 void Widget::set_sync_id(string id)
409 {
410 ASSERT(!_get_parent()); // synced widgets are collected on layout push/pop
411 m_sync_id = id;
412 }
413
414 #ifdef USE_TILE_WEB
sync_save_state()415 void Widget::sync_save_state()
416 {
417 }
418
sync_load_state(const JsonNode *)419 void Widget::sync_load_state(const JsonNode *)
420 {
421 }
422
sync_state_changed()423 void Widget::sync_state_changed()
424 {
425 if (m_sync_id.empty())
426 return;
427 const auto& top = ui_root.top_child();
428 if (!top || !top->is_ancestor_of(get_shared()))
429 return;
430 tiles.json_open_object();
431 sync_save_state();
432 tiles.json_write_string("msg", "ui-state-sync");
433 tiles.json_write_string("widget_id", m_sync_id);
434 tiles.json_write_bool("from_webtiles", ui_root.receiving_ui_state);
435 tiles.json_write_int("generation_id", ui_root.state.generation_id);
436 tiles.json_close_object();
437 tiles.finish_message();
438 }
439 #endif
440
add_child(shared_ptr<Widget> child)441 void Box::add_child(shared_ptr<Widget> child)
442 {
443 child->_set_parent(this);
444 m_children.push_back(move(child));
445 _invalidate_sizereq();
446 }
447
_render()448 void Box::_render()
449 {
450 for (auto const& child : m_children)
451 child->render();
452 }
453
layout_main_axis(vector<SizeReq> & ch_psz,int main_sz)454 vector<int> Box::layout_main_axis(vector<SizeReq>& ch_psz, int main_sz)
455 {
456 // find the child sizes on the main axis
457 vector<int> ch_sz(m_children.size());
458
459 int extra = main_sz;
460 for (size_t i = 0; i < m_children.size(); i++)
461 {
462 ch_sz[i] = ch_psz[i].min;
463 extra -= ch_psz[i].min;
464 if (align_main == Align::STRETCH)
465 ch_psz[i].nat = INT_MAX;
466 }
467 ASSERT(extra >= 0);
468
469 while (extra > 0)
470 {
471 int sum_flex_grow = 0, remainder = 0;
472 for (size_t i = 0; i < m_children.size(); i++)
473 sum_flex_grow += ch_sz[i] < ch_psz[i].nat ? m_children[i]->flex_grow : 0;
474 if (!sum_flex_grow)
475 break;
476
477 // distribute space to children, based on flex_grow
478 for (size_t i = 0; i < m_children.size(); i++)
479 {
480 float efg = ch_sz[i] < ch_psz[i].nat ? m_children[i]->flex_grow : 0;
481 int ch_extra = extra * efg / sum_flex_grow;
482 ASSERT(ch_sz[i] <= ch_psz[i].nat);
483 int taken = min(ch_extra, ch_psz[i].nat - ch_sz[i]);
484 ch_sz[i] += taken;
485 remainder += ch_extra - taken;
486 }
487 extra = remainder;
488 }
489
490 return ch_sz;
491 }
492
layout_cross_axis(vector<SizeReq> & ch_psz,int cross_sz)493 vector<int> Box::layout_cross_axis(vector<SizeReq>& ch_psz, int cross_sz)
494 {
495 vector<int> ch_sz(m_children.size());
496
497 for (size_t i = 0; i < m_children.size(); i++)
498 {
499 const bool stretch = align_cross == STRETCH;
500 ch_sz[i] = stretch ? cross_sz : min(max(ch_psz[i].min, cross_sz), ch_psz[i].nat);
501 }
502
503 return ch_sz;
504 }
505
_get_preferred_size(Direction dim,int prosp_width)506 SizeReq Box::_get_preferred_size(Direction dim, int prosp_width)
507 {
508 vector<SizeReq> sr(m_children.size());
509
510 // Get preferred widths
511 for (size_t i = 0; i < m_children.size(); i++)
512 sr[i] = m_children[i]->get_preferred_size(Widget::HORZ, -1);
513
514 if (dim)
515 {
516 // Get actual widths
517 vector<int> cw = horz ? layout_main_axis(sr, prosp_width) : layout_cross_axis(sr, prosp_width);
518
519 // Get preferred heights
520 for (size_t i = 0; i < m_children.size(); i++)
521 sr[i] = m_children[i]->get_preferred_size(Widget::VERT, cw[i]);
522 }
523
524 // find sum/max of preferred sizes, as appropriate
525 bool main_axis = dim == !horz;
526 SizeReq r = { 0, 0 };
527 for (auto const& c : sr)
528 {
529 r.min = main_axis ? r.min + c.min : max(r.min, c.min);
530 r.nat = main_axis ? r.nat + c.nat : max(r.nat, c.nat);
531 }
532 return r;
533 }
534
_allocate_region()535 void Box::_allocate_region()
536 {
537 vector<SizeReq> sr(m_children.size());
538
539 // Get preferred widths
540 for (size_t i = 0; i < m_children.size(); i++)
541 sr[i] = m_children[i]->get_preferred_size(Widget::HORZ, -1);
542
543 // Get actual widths
544 vector<int> cw = horz ? layout_main_axis(sr, m_region.width) : layout_cross_axis(sr, m_region.width);
545
546 // Get preferred heights
547 for (size_t i = 0; i < m_children.size(); i++)
548 sr[i] = m_children[i]->get_preferred_size(Widget::VERT, cw[i]);
549
550 // Get actual heights
551 vector<int> ch = horz ? layout_cross_axis(sr, m_region.height) : layout_main_axis(sr, m_region.height);
552
553 auto const &m = horz ? cw : ch;
554 int extra_main_space = (horz ? m_region.width : m_region.height) - accumulate(m.begin(), m.end(), 0);
555 ASSERT(extra_main_space >= 0);
556
557 // main axis offset
558 int mo;
559 switch (align_main)
560 {
561 case Widget::START: mo = 0; break;
562 case Widget::CENTER: mo = extra_main_space/2; break;
563 case Widget::END: mo = extra_main_space; break;
564 case Widget::STRETCH: mo = 0; break;
565 default: ASSERT(0);
566 }
567 int ho = m_region.x + (horz ? mo : 0);
568 int vo = m_region.y + (!horz ? mo : 0);
569
570 Region cr = {ho, vo, 0, 0};
571 for (size_t i = 0; i < m_children.size(); i++)
572 {
573 // cross axis offset
574 int extra_cross_space = horz ? m_region.height - ch[i] : m_region.width - cw[i];
575
576 const Align child_align = align_cross;
577 int xo;
578 switch (child_align)
579 {
580 case Widget::START: xo = 0; break;
581 case Widget::CENTER: xo = extra_cross_space/2; break;
582 case Widget::END: xo = extra_cross_space; break;
583 case Widget::STRETCH: xo = 0; break;
584 default: ASSERT(0);
585 }
586
587 int& cr_cross_offset = horz ? cr.y : cr.x;
588 int& cr_cross_size = horz ? cr.height : cr.width;
589
590 cr_cross_offset = (horz ? vo : ho) + xo;
591 cr.width = cw[i];
592 cr.height = ch[i];
593 if (child_align == STRETCH)
594 cr_cross_size = (horz ? ch : cw)[i];
595 m_children[i]->allocate_region(cr);
596
597 int& cr_main_offset = horz ? cr.x : cr.y;
598 int& cr_main_size = horz ? cr.width : cr.height;
599 cr_main_offset += cr_main_size;
600 }
601 }
602
Text()603 Text::Text()
604 {
605 #ifdef USE_TILE_LOCAL
606 set_font(tiles.get_crt_font());
607 #endif
608 }
609
set_text(const formatted_string & fs)610 void Text::set_text(const formatted_string &fs)
611 {
612 if (fs == m_text)
613 return;
614 m_text.clear();
615 m_text += fs;
616 _invalidate_sizereq();
617 _expose();
618 m_wrapped_size = Size(-1);
619 _queue_allocation();
620 }
621
622 #ifdef USE_TILE_LOCAL
set_font(FontWrapper * font)623 void Text::set_font(FontWrapper *font)
624 {
625 ASSERT(font);
626 m_font = font;
627 _queue_allocation();
628 }
629 #endif
630
set_highlight_pattern(string pattern,bool line)631 void Text::set_highlight_pattern(string pattern, bool line)
632 {
633 hl_pat = pattern;
634 hl_line = line;
635 _expose();
636 }
637
wrap_text_to_size(int width,int height)638 void Text::wrap_text_to_size(int width, int height)
639 {
640 // don't recalculate if the previous calculation imposed no constraints
641 // on the text, and the new calculation would be the same or greater in
642 // size. This can happen through a sequence of _get_preferred_size calls
643 // for example.
644 if (m_wrapped_size.is_valid())
645 {
646 const bool cached_width_max = m_wrapped_sizereq.width <= 0
647 || m_wrapped_sizereq.width > m_wrapped_size.width;
648 const bool cached_height_max = m_wrapped_sizereq.height <= 0
649 || m_wrapped_sizereq.height > m_wrapped_size.height;
650 if ((width == m_wrapped_size.width || cached_width_max && (width <= 0 || width >= m_wrapped_size.width))
651 && (height == m_wrapped_size.height || cached_height_max && (height <= 0 || height >= m_wrapped_size.height)))
652 {
653 return;
654 }
655 }
656
657 m_wrapped_sizereq = Size(width, height);
658
659 height = height ? height : 0xfffffff;
660
661 #ifdef USE_TILE_LOCAL
662 if (wrap_text || ellipsize)
663 m_text_wrapped = m_font->split(m_text, width, height);
664 else
665 m_text_wrapped = m_text;
666
667 m_brkpts.clear();
668 m_brkpts.emplace_back(brkpt({0, 0}));
669 unsigned tally = 0, acc = 0;
670 for (unsigned i = 0; i < m_text_wrapped.ops.size(); i++)
671 {
672 formatted_string::fs_op &op = m_text_wrapped.ops[i];
673 if (op.type != FSOP_TEXT)
674 continue;
675 if (acc > 0)
676 {
677 m_brkpts.emplace_back(brkpt({i, tally}));
678 acc = 0;
679 }
680 unsigned n = count(op.text.begin(), op.text.end(), '\n');
681 acc += n;
682 tally += n;
683 }
684
685 // if the request was to figure out the height, record the height found
686 m_wrapped_size.height = m_font->string_height(m_text_wrapped);
687 m_wrapped_size.width = m_font->string_width(m_text_wrapped);
688 #else
689 m_wrapped_lines.clear();
690 formatted_string::parse_string_to_multiple(m_text.to_colour_string(), m_wrapped_lines, width);
691 // add ellipsis to last line of text if necessary
692 if (height < (int)m_wrapped_lines.size())
693 {
694 auto& last_line = m_wrapped_lines[height-1], next_line = m_wrapped_lines[height];
695 last_line += " ";
696 last_line += next_line;
697 last_line = last_line.chop(width-2);
698 last_line += "..";
699 m_wrapped_lines.resize(height);
700 }
701 if (m_wrapped_lines.empty())
702 m_wrapped_lines.emplace_back("");
703
704 m_wrapped_size.height = m_wrapped_lines.size();
705 if (width <= 0)
706 {
707 // only bother recalculating if there was no requested width --
708 // parse_string_to_multiple will exactly obey any explicit width value
709 int max_width = 0;
710 for (auto &fs : m_wrapped_lines)
711 max_width = max(max_width, fs.width());
712 m_wrapped_size.width = max_width;
713 }
714 else
715 m_wrapped_size.width = width;
716 #endif
717 }
718
_find_highlights(const string & haystack,const string & needle,int a,int b)719 static vector<size_t> _find_highlights(const string& haystack, const string& needle, int a, int b)
720 {
721 vector<size_t> highlights;
722 size_t pos = haystack.find(needle, max(a-(int)needle.size()+1, 0));
723 while (pos != string::npos && pos < b+needle.size()-1)
724 {
725 highlights.push_back(pos);
726 pos = haystack.find(needle, pos+1);
727 }
728 return highlights;
729 }
730
_render()731 void Text::_render()
732 {
733 Region region = m_region;
734 region = region.aabb_intersect(scissor_stack.top());
735 if (region.width <= 0 || region.height <= 0)
736 return;
737
738 wrap_text_to_size(m_region.width, m_region.height);
739
740 #ifdef USE_TILE_LOCAL
741 const int dev_line_height = m_font->char_height(false);
742 const int line_min_pos = display_density.logical_to_device(
743 region.y - m_region.y);
744 const int line_max_pos = display_density.logical_to_device(
745 region.y + region.height - m_region.y);
746 const int line_min = line_min_pos / dev_line_height;
747 const int line_max = line_max_pos / dev_line_height;
748
749 // find the earliest and latest ops in the string that could be displayed
750 // in the currently visible region, as well as the line offset of the
751 // earliest.
752 int line_off = 0;
753 int ops_min = 0, ops_max = m_text_wrapped.ops.size();
754 {
755 int i = 1;
756 for (; i < (int) m_brkpts.size(); i++)
757 if (static_cast<int>(m_brkpts[i].line) >= line_min)
758 {
759 ops_min = m_brkpts[i - 1].op;
760 line_off = m_brkpts[i - 1].line;
761 break;
762 }
763 for (; i < (int)m_brkpts.size(); i++)
764 if (static_cast<int>(m_brkpts[i].line) > line_max)
765 {
766 ops_max = m_brkpts[i].op;
767 break;
768 }
769 }
770
771 // the slice of the contained string that will be displayed
772 formatted_string slice;
773 slice.ops = vector<formatted_string::fs_op>(
774 m_text_wrapped.ops.begin() + ops_min,
775 m_text_wrapped.ops.begin() + ops_max);
776
777 // TODO: this is really complicated, can it be refactored? Does it
778 // really need to iterate over the formatted_text slices? I'm not sure
779 // the formatting ever matters for highlighting locations.
780 if (!hl_pat.empty())
781 {
782 const auto& full_text = m_text.tostring();
783
784 // need to find the byte ranges in full_text that our slice corresponds to
785 // note that the indexes are the same in both m_text and m_text_wrapped
786 // only because wordwrapping only replaces ' ' with '\n': in other words,
787 // this is fairly brittle
788 ASSERT(full_text.size() == m_text_wrapped.tostring().size());
789 // index of the first and last ops that are being displayed
790 int begin_idx = ops_min == 0 ?
791 0 :
792 m_text_wrapped.tostring(0, ops_min - 1).size();
793 int end_idx = begin_idx
794 + m_text_wrapped.tostring(ops_min, ops_max-1).size();
795
796 vector<size_t> highlights = _find_highlights(full_text, hl_pat,
797 begin_idx, end_idx);
798
799 int ox = m_region.x;
800 const int oy = display_density.logical_to_device(m_region.y) +
801 dev_line_height * line_off;
802 size_t lacc = 0; // the start char of the current op relative to region
803 size_t line = 0; // the line we are at relative to the region
804
805 bool inside = false;
806 // Iterate over formatted_string op slices, looking for highlight
807 // sequences. Highlight sequences may span multiple op slices, hence
808 // some of the complexity here.
809 // All the y math in this section needs to be done in device pixels,
810 // in order to handle fractional advances.
811 set<size_t> block_lines;
812 for (unsigned i = 0; i < slice.ops.size() && !highlights.empty(); i++)
813 {
814 const auto& op = slice.ops[i];
815 if (op.type != FSOP_TEXT)
816 continue;
817 size_t oplen = op.text.size();
818
819 // dimensions in chars of the pattern relative to current op
820 size_t start = highlights[0] - begin_idx - lacc;
821 size_t end = highlights[0] - begin_idx - lacc + hl_pat.size();
822
823 size_t op_line_start = begin_idx + lacc > 0
824 ? full_text.rfind('\n', begin_idx + lacc - 1)
825 : string::npos;
826 op_line_start = op_line_start == string::npos
827 ? 0
828 : op_line_start + 1;
829 string line_before_op = full_text.substr(op_line_start,
830 begin_idx + lacc - op_line_start);
831 // pixel positions for the current op
832 size_t op_x = m_font->string_width(line_before_op.c_str());
833 const size_t op_y =
834 m_font->string_height(line_before_op.c_str(), false)
835 - dev_line_height;
836
837 // positions in device pixels to highlight relative to current op
838 size_t sx = 0, ex = m_font->string_width(op.text.c_str());
839 size_t sy = 0, ey = dev_line_height;
840
841 bool started = false; // does the highlight start in the current op?
842 bool ended = false; // does the highlight end in the current op?
843
844 if (start < oplen) // assume start is unsigned and so >=0
845 {
846 // hacky: reset op x to 0 if we've hit a linebreak in the op
847 // before start. This is to handle cases where the op starts
848 // midline, but the pattern is after a linebreak in the op.
849 const size_t linebreak_in_op = op.text.find("\n");
850 if (start > linebreak_in_op)
851 op_x = 0;
852 // start position is somewhere in the current op
853 const string before = full_text.substr(begin_idx + lacc, start);
854 sx = m_font->string_width(before.c_str());
855 sy = m_font->string_height(before.c_str(), false)
856 - dev_line_height;
857 started = true;
858 }
859 if (end <= oplen) // assume end is unsigned and so >=0
860 {
861 const string to_end = full_text.substr(begin_idx + lacc, end);
862 ex = m_font->string_width(to_end.c_str());
863 ey = m_font->string_height(to_end.c_str(), false);
864 ended = true;
865 }
866
867 if (started || ended || inside)
868 {
869 m_hl_buf.clear();
870 // TODO: as far as I can tell, the above code never produces
871 // multi-line spans for this to iterate over...
872 for (size_t y = oy + op_y + line + sy;
873 y < oy + op_y + line + ey;
874 y += dev_line_height)
875 {
876 if (block_lines.count(y)) // kind of brittle...
877 continue;
878 if (hl_line)
879 {
880 block_lines.insert(y);
881 m_hl_buf.add(region.x,
882 display_density.device_to_logical(y),
883 region.x + region.width,
884 display_density.device_to_logical(y
885 + dev_line_height),
886 VColour(255, 255, 0, 50));
887 }
888 else
889 {
890 m_hl_buf.add(ox + op_x + sx,
891 display_density.device_to_logical(y),
892 ox + op_x + ex,
893 display_density.device_to_logical(y
894 + dev_line_height),
895 VColour(255, 255, 0, 50));
896 }
897 }
898 m_hl_buf.draw();
899 }
900 inside = !ended && (inside || started);
901
902 if (ended)
903 {
904 highlights.erase(highlights.begin() + 0);
905 i--;
906 }
907 else
908 {
909 lacc += oplen;
910 line += m_font->string_height(op.text.c_str(), false)
911 - dev_line_height;
912 }
913 }
914 }
915
916 // XXX: should be moved into a new function render_formatted_string()
917 // in FTFontWrapper, that, like render_textblock(), would automatically
918 // handle swapping atlas glyphs as necessary.
919 FontBuffer m_font_buf(m_font);
920 m_font_buf.add(slice, m_region.x, m_region.y +
921 display_density.device_to_logical(dev_line_height * line_off));
922 m_font_buf.draw();
923 #else
924 const auto& lines = m_wrapped_lines;
925 vector<size_t> highlights;
926 int begin_idx = 0;
927
928 clear_text_region(m_region, m_bg_colour);
929
930 if (!hl_pat.empty())
931 {
932 for (int i = 0; i < region.y-m_region.y; i++)
933 begin_idx += m_wrapped_lines[i].tostring().size()+1;
934 int end_idx = begin_idx;
935 for (int i = region.y-m_region.y; i < region.y-m_region.y+region.height; i++)
936 end_idx += m_wrapped_lines[i].tostring().size()+1;
937 highlights = _find_highlights(m_text.tostring(), hl_pat, begin_idx, end_idx);
938 }
939
940 unsigned int hl_idx = 0;
941 for (size_t i = 0; i < min(lines.size(), (size_t)region.height); i++)
942 {
943 cgotoxy(region.x+1, region.y+1+i);
944 formatted_string line = lines[i+region.y-m_region.y];
945 int end_idx = begin_idx + line.tostring().size();
946
947 // convert highlights on this line to a list of line cuts
948 vector<size_t> cuts = {0};
949 for (; hl_idx < highlights.size() && (int)highlights[hl_idx] < end_idx; hl_idx++)
950 {
951 ASSERT(highlights[hl_idx]+hl_pat.size() >= (size_t)begin_idx);
952 int la = max((int)highlights[hl_idx] - begin_idx, 0);
953 int lb = min(highlights[hl_idx]+hl_pat.size() - begin_idx, (size_t)end_idx - begin_idx);
954 ASSERT(la < lb);
955 cuts.push_back(la);
956 cuts.push_back(lb);
957 }
958 cuts.push_back(end_idx - begin_idx);
959
960 // keep the last highlight if it extend into the next line
961 if (hl_idx && highlights[hl_idx-1]+hl_pat.size() > (size_t)end_idx)
962 hl_idx--;
963
964 // cut the line, and highlight alternate segments
965 formatted_string out;
966 for (size_t j = 0; j+1 < cuts.size(); j++)
967 {
968 formatted_string slice = line.substr_bytes(cuts[j], cuts[j+1]-cuts[j]);
969 if (j%2)
970 {
971 out.textcolour(WHITE);
972 out.cprintf("%s", slice.tostring().c_str());
973 }
974 else
975 out += slice;
976 }
977 out.chop(region.width).display(0);
978
979 begin_idx = end_idx + 1; // +1 is for the newline
980 }
981 #endif
982 }
983
_get_preferred_size(Direction dim,int prosp_width)984 SizeReq Text::_get_preferred_size(Direction dim, int prosp_width)
985 {
986 #ifdef USE_TILE_LOCAL
987 if (!dim)
988 {
989 int w = m_font->string_width(m_text);
990 // XXX: should be width of '..', unless string itself is shorter than '..'
991 static constexpr int min_ellipsized_width = 0;
992 static constexpr int min_wrapped_width = 0; // XXX: should be width of longest word
993 return { ellipsize ? min_ellipsized_width : wrap_text ? min_wrapped_width : w, w };
994 }
995 else
996 {
997 wrap_text_to_size(prosp_width, 0);
998 int height = m_font->string_height(m_text_wrapped);
999 return { ellipsize ? (int)m_font->char_height() : height, height };
1000 }
1001 #else
1002 if (!dim)
1003 {
1004 int w = 0, line_w = 0;
1005 for (auto const& ch : m_text.tostring())
1006 {
1007 w = ch == '\n' ? max(w, line_w) : w;
1008 line_w = ch == '\n' ? 0 : line_w+1;
1009 }
1010 w = max(w, line_w);
1011
1012 // XXX: should be width of '..', unless string itself is shorter than '..'
1013 static constexpr int min_ellipsized_width = 0;
1014 static constexpr int min_wrapped_width = 0; // XXX: should be char width of longest word in text
1015 return { ellipsize ? min_ellipsized_width : wrap_text ? min_wrapped_width : w, w };
1016 }
1017 else
1018 {
1019 wrap_text_to_size(prosp_width, 0);
1020 int height = m_wrapped_lines.size();
1021 return { ellipsize ? 1 : height, height };
1022 }
1023 #endif
1024 }
1025
_allocate_region()1026 void Text::_allocate_region()
1027 {
1028 wrap_text_to_size(m_region.width, m_region.height);
1029 }
1030
1031 #ifndef USE_TILE_LOCAL
set_bg_colour(COLOURS colour)1032 void Text::set_bg_colour(COLOURS colour)
1033 {
1034 m_bg_colour = colour;
1035 _expose();
1036 }
1037 #endif
1038
set_tile(tile_def tile)1039 void Image::set_tile(tile_def tile)
1040 {
1041 #ifdef USE_TILE
1042 m_tile = tile;
1043 #ifdef USE_TILE_LOCAL
1044 const tile_info &ti = tiles.get_image_manager()->tile_def_info(m_tile);
1045 m_tw = ti.width;
1046 m_th = ti.height;
1047 _invalidate_sizereq();
1048 #endif
1049 #else
1050 UNUSED(tile);
1051 #endif
1052 }
1053
_render()1054 void Image::_render()
1055 {
1056 #ifdef USE_TILE_LOCAL
1057 scissor_stack.push(m_region);
1058 TileBuffer tb;
1059 tb.set_tex(&tiles.get_image_manager()->m_textures[get_tile_texture(m_tile.tile)]);
1060
1061 for (int y = m_region.y; y < m_region.y+m_region.height; y+=m_th)
1062 for (int x = m_region.x; x < m_region.x+m_region.width; x+=m_tw)
1063 tb.add(m_tile.tile, x, y, 0, 0, false, m_th, 1.0, 1.0);
1064
1065 tb.draw();
1066 tb.clear();
1067 scissor_stack.pop();
1068 #endif
1069 }
1070
_get_preferred_size(Direction dim,int)1071 SizeReq Image::_get_preferred_size(Direction dim, int /*prosp_width*/)
1072 {
1073 #ifdef USE_TILE_LOCAL
1074 return {
1075 // expand takes precedence over shrink for historical reasons
1076 dim ? (shrink_v ? 0 : m_th) : (shrink_h ? 0 : m_tw),
1077 dim ? (shrink_v ? 0 : m_th) : (shrink_h ? 0 : m_tw)
1078 };
1079 #else
1080 UNUSED(dim);
1081 return { 0, 0 };
1082 #endif
1083 }
1084
add_child(shared_ptr<Widget> child)1085 void Stack::add_child(shared_ptr<Widget> child)
1086 {
1087 child->_set_parent(this);
1088 m_children.push_back(move(child));
1089 _invalidate_sizereq();
1090 _queue_allocation();
1091 }
1092
pop_child()1093 void Stack::pop_child()
1094 {
1095 if (!m_children.size())
1096 return;
1097 m_children.pop_back();
1098 _invalidate_sizereq();
1099 _queue_allocation();
1100 }
1101
get_child_at_offset(int x,int y)1102 shared_ptr<Widget> Stack::get_child_at_offset(int x, int y)
1103 {
1104 if (m_children.size() == 0)
1105 return nullptr;
1106 bool inside = m_children.back()->get_region().contains_point(x, y);
1107 return inside ? m_children.back() : nullptr;
1108 }
1109
_render()1110 void Stack::_render()
1111 {
1112 for (auto const& child : m_children)
1113 child->render();
1114 }
1115
_get_preferred_size(Direction dim,int prosp_width)1116 SizeReq Stack::_get_preferred_size(Direction dim, int prosp_width)
1117 {
1118 SizeReq r = { 0, 0 };
1119 for (auto const& child : m_children)
1120 {
1121 SizeReq c = child->get_preferred_size(dim, prosp_width);
1122 r.min = max(r.min, c.min);
1123 r.nat = max(r.nat, c.nat);
1124 }
1125 return r;
1126 }
1127
_allocate_region()1128 void Stack::_allocate_region()
1129 {
1130 for (auto const& child : m_children)
1131 {
1132 Region cr = m_region;
1133 SizeReq pw = child->get_preferred_size(Widget::HORZ, -1);
1134 cr.width = min(max(pw.min, m_region.width), pw.nat);
1135 SizeReq ph = child->get_preferred_size(Widget::VERT, cr.width);
1136 cr.height = min(max(ph.min, m_region.height), ph.nat);
1137 child->allocate_region(cr);
1138 }
1139 }
1140
add_child(shared_ptr<Widget> child)1141 void Switcher::add_child(shared_ptr<Widget> child)
1142 {
1143 // TODO XXX: if there's a focused widget
1144 // - it must be in the current top child
1145 // - unfocus it before we
1146 child->_set_parent(this);
1147 m_children.push_back(move(child));
1148 _invalidate_sizereq();
1149 _queue_allocation();
1150 }
1151
current()1152 int& Switcher::current()
1153 {
1154 // TODO XXX: we need to update the focused widget
1155 // so we need an API change
1156 _expose();
1157 return m_current;
1158 }
1159
current_widget()1160 shared_ptr<Widget> Switcher::current_widget()
1161 {
1162 if (m_children.size() == 0)
1163 return nullptr;
1164 m_current = max(0, min(m_current, (int)m_children.size()-1));
1165 return m_children[m_current];
1166 }
1167
_render()1168 void Switcher::_render()
1169 {
1170 if (m_children.size() == 0)
1171 return;
1172 m_current = max(0, min(m_current, (int)m_children.size()-1));
1173 m_children[m_current]->render();
1174 }
1175
_get_preferred_size(Direction dim,int prosp_width)1176 SizeReq Switcher::_get_preferred_size(Direction dim, int prosp_width)
1177 {
1178 SizeReq r = { 0, 0 };
1179 for (auto const& child : m_children)
1180 {
1181 SizeReq c = child->get_preferred_size(dim, prosp_width);
1182 r.min = max(r.min, c.min);
1183 r.nat = max(r.nat, c.nat);
1184 }
1185 return r;
1186 }
1187
_allocate_region()1188 void Switcher::_allocate_region()
1189 {
1190 for (auto const& child : m_children)
1191 {
1192 Region cr = m_region;
1193 SizeReq pw = child->get_preferred_size(Widget::HORZ, -1);
1194 cr.width = min(max(pw.min, m_region.width), pw.nat);
1195 SizeReq ph = child->get_preferred_size(Widget::VERT, cr.width);
1196 cr.height = min(max(ph.min, m_region.height), ph.nat);
1197 int xo, yo;
1198 switch (align_x)
1199 {
1200 case Widget::START: xo = 0; break;
1201 case Widget::CENTER: xo = (m_region.width - cr.width)/2; break;
1202 case Widget::END: xo = m_region.width - cr.width; break;
1203 case Widget::STRETCH: xo = 0; break;
1204 default: ASSERT(0);
1205 }
1206 switch (align_y)
1207 {
1208 case Widget::START: yo = 0; break;
1209 case Widget::CENTER: yo = (m_region.height - cr.height)/2; break;
1210 case Widget::END: yo = m_region.height - cr.height; break;
1211 case Widget::STRETCH: yo = 0; break;
1212 default: ASSERT(0);
1213 }
1214 cr.width += xo;
1215 cr.height += yo;
1216 if (align_x == Widget::STRETCH)
1217 cr.width = m_region.width;
1218 if (align_y == Widget::STRETCH)
1219 cr.height = m_region.height;
1220 child->allocate_region(cr);
1221 }
1222 }
1223
get_child_at_offset(int x,int y)1224 shared_ptr<Widget> Switcher::get_child_at_offset(int x, int y)
1225 {
1226 if (m_children.size() == 0)
1227 return nullptr;
1228
1229 int c = max(0, min(m_current, (int)m_children.size()));
1230 bool inside = m_children[c]->get_region().contains_point(x, y);
1231 return inside ? m_children[c] : nullptr;
1232 }
1233
get_child_at_offset(int x,int y)1234 shared_ptr<Widget> Grid::get_child_at_offset(int x, int y)
1235 {
1236 int lx = x - m_region.x;
1237 int ly = y - m_region.y;
1238 int row = -1, col = -1;
1239 for (int i = 0; i < (int)m_col_info.size(); i++)
1240 {
1241 const auto& tr = m_col_info[i];
1242 if (lx >= tr.offset && lx < tr.offset + tr.size)
1243 {
1244 col = i;
1245 break;
1246 }
1247 }
1248 for (int i = 0; i < (int)m_row_info.size(); i++)
1249 {
1250 const auto& tr = m_row_info[i];
1251 if (ly >= tr.offset && ly < tr.offset + tr.size)
1252 {
1253 row = i;
1254 break;
1255 }
1256 }
1257 if (row == -1 || col == -1)
1258 return nullptr;
1259 for (auto& child : m_child_info)
1260 {
1261 if (child.pos.x <= col && col < child.pos.x + child.span.width)
1262 if (child.pos.y <= row && row < child.pos.y + child.span.height)
1263 if (child.widget->get_region().contains_point(x, y))
1264 return child.widget;
1265 }
1266 return nullptr;
1267 }
1268
add_child(shared_ptr<Widget> child,int x,int y,int w,int h)1269 void Grid::add_child(shared_ptr<Widget> child, int x, int y, int w, int h)
1270 {
1271 child->_set_parent(this);
1272 child_info ch = { {x, y}, {w, h}, move(child) };
1273 m_child_info.push_back(ch);
1274 m_track_info_dirty = true;
1275 _invalidate_sizereq();
1276 }
1277
init_track_info()1278 void Grid::init_track_info()
1279 {
1280 if (!m_track_info_dirty)
1281 return;
1282 m_track_info_dirty = false;
1283
1284 // calculate the number of rows and columns
1285 int n_rows = 0, n_cols = 0;
1286 for (auto info : m_child_info)
1287 {
1288 n_rows = max(n_rows, info.pos.y+info.span.height);
1289 n_cols = max(n_cols, info.pos.x+info.span.width);
1290 }
1291 m_row_info.resize(n_rows);
1292 m_col_info.resize(n_cols);
1293
1294 sort(m_child_info.begin(), m_child_info.end(),
1295 [](const child_info& a, const child_info& b) {
1296 return a.pos.y < b.pos.y;
1297 });
1298 }
1299
_render()1300 void Grid::_render()
1301 {
1302 // Find the visible rows
1303 const auto scissor = scissor_stack.top();
1304 int row_min = 0, row_max = m_row_info.size()-1, i = 0;
1305 for (; i < (int)m_row_info.size(); i++)
1306 if (m_row_info[i].offset+m_row_info[i].size+m_region.y >= scissor.y)
1307 {
1308 row_min = i;
1309 break;
1310 }
1311 for (; i < (int)m_row_info.size(); i++)
1312 if (m_row_info[i].offset+m_region.y >= scissor.ey())
1313 {
1314 row_max = i-1;
1315 break;
1316 }
1317
1318 for (auto const& child : m_child_info)
1319 {
1320 if (child.pos.y < row_min) continue;
1321 if (child.pos.y > row_max) break;
1322 child.widget->render();
1323 }
1324 }
1325
compute_track_sizereqs(Direction dim)1326 void Grid::compute_track_sizereqs(Direction dim)
1327 {
1328 auto& track = dim ? m_row_info : m_col_info;
1329 #define DIV_ROUND_UP(n, d) (((n)+(d)-1)/(d))
1330
1331 for (auto& t : track)
1332 t.sr = {0, 0};
1333 for (size_t i = 0; i < m_child_info.size(); i++)
1334 {
1335 auto& cp = m_child_info[i].pos;
1336 auto& cs = m_child_info[i].span;
1337 // if merging horizontally, need to find (possibly multi-col) width
1338 int prosp_width = dim ? get_tracks_region(cp.x, cp.y, cs.width, cs.height).width : -1;
1339
1340 const SizeReq c = m_child_info[i].widget->get_preferred_size(dim, prosp_width);
1341 // NOTE: items spanning multiple rows/cols don't contribute!
1342 if (cs.width == 1 && cs.height == 1)
1343 {
1344 auto& s = track[dim ? cp.y : cp.x].sr;
1345 s.min = max(s.min, c.min);
1346 s.nat = max(s.nat, c.nat);
1347 }
1348 }
1349 }
1350
set_track_offsets(vector<track_info> & tracks)1351 void Grid::set_track_offsets(vector<track_info>& tracks)
1352 {
1353 int acc = 0;
1354 for (auto& track : tracks)
1355 {
1356 track.offset = acc;
1357 acc += track.size;
1358 }
1359 }
1360
_get_preferred_size(Direction dim,int prosp_width)1361 SizeReq Grid::_get_preferred_size(Direction dim, int prosp_width)
1362 {
1363 init_track_info();
1364
1365 // get preferred column widths
1366 compute_track_sizereqs(Widget::HORZ);
1367
1368 // total width min and nat
1369 SizeReq w_sr = { 0, 0 };
1370 for (auto const& col : m_col_info)
1371 {
1372 w_sr.min += col.sr.min;
1373 w_sr.nat += col.sr.nat;
1374 }
1375
1376 if (!dim)
1377 return w_sr;
1378
1379 layout_track(Widget::HORZ, w_sr, prosp_width);
1380 set_track_offsets(m_col_info);
1381
1382 // get preferred row heights for those widths
1383 compute_track_sizereqs(Widget::VERT);
1384
1385 // total height min and nat
1386 SizeReq h_sr = { 0, 0 };
1387 for (auto const& row : m_row_info)
1388 {
1389 h_sr.min += row.sr.min;
1390 h_sr.nat += row.sr.nat;
1391 }
1392
1393 return h_sr;
1394 }
1395
layout_track(Direction dim,SizeReq sr,int size)1396 void Grid::layout_track(Direction dim, SizeReq sr, int size)
1397 {
1398 auto& infos = dim ? m_row_info : m_col_info;
1399
1400 int extra = size - sr.min;
1401 ASSERT(extra >= 0);
1402
1403 for (size_t i = 0; i < infos.size(); ++i)
1404 infos[i].size = infos[i].sr.min;
1405
1406 const bool stretch = dim ? stretch_v : stretch_h;
1407 bool stretching = false;
1408
1409 while (true)
1410 {
1411 int sum_flex_grow = 0, sum_taken = 0;
1412 for (const auto& info : infos)
1413 sum_flex_grow += info.size < info.sr.nat ? info.flex_grow : 0;
1414 if (!sum_flex_grow)
1415 {
1416 if (!stretch)
1417 break;
1418 stretching = true;
1419 for (const auto& info : infos)
1420 sum_flex_grow += info.flex_grow;
1421 if (!sum_flex_grow)
1422 break;
1423 }
1424
1425 for (size_t i = 0; i < infos.size(); ++i)
1426 {
1427 float efg = (infos[i].size < infos[i].sr.nat || stretching)
1428 ? infos[i].flex_grow : 0;
1429 int tr_extra = extra * efg / sum_flex_grow;
1430 ASSERT(stretching || infos[i].size <= infos[i].sr.nat);
1431 int taken = stretching ? tr_extra
1432 : min(tr_extra, infos[i].sr.nat - infos[i].size);
1433 infos[i].size += taken;
1434 sum_taken += taken;
1435 }
1436 if (!sum_taken)
1437 break;
1438 extra = extra - sum_taken;
1439 }
1440 }
1441
_allocate_region()1442 void Grid::_allocate_region()
1443 {
1444 // Use of _-prefixed member function is necessary here
1445 SizeReq h_sr = _get_preferred_size(Widget::VERT, m_region.width);
1446
1447 layout_track(Widget::VERT, h_sr, m_region.height);
1448 set_track_offsets(m_row_info);
1449
1450 for (size_t i = 0; i < m_child_info.size(); i++)
1451 {
1452 auto& cp = m_child_info[i].pos;
1453 auto& cs = m_child_info[i].span;
1454 Region cell_reg = get_tracks_region(cp.x, cp.y, cs.width, cs.height);
1455 cell_reg.x += m_region.x;
1456 cell_reg.y += m_region.y;
1457 m_child_info[i].widget->allocate_region(cell_reg);
1458 }
1459 }
1460
set_scroll(int y)1461 void Scroller::set_scroll(int y)
1462 {
1463 if (m_scroll == y)
1464 return;
1465 m_scroll = y;
1466 _queue_allocation();
1467 #ifdef USE_TILE_WEB
1468 tiles.json_open_object();
1469 tiles.json_write_string("msg", "ui-scroller-scroll");
1470 // XXX: always false, since we do not yet synchronize
1471 // webtiles client-side scrolls
1472 tiles.json_write_bool("from_webtiles", false);
1473 tiles.json_write_int("scroll", y);
1474 tiles.json_close_object();
1475 tiles.finish_message();
1476 #endif
1477 }
1478
_render()1479 void Scroller::_render()
1480 {
1481 if (m_child)
1482 {
1483 scissor_stack.push(m_region);
1484 m_child->render();
1485 #ifdef USE_TILE_LOCAL
1486 m_shade_buf.draw();
1487 #endif
1488 scissor_stack.pop();
1489 #ifdef USE_TILE_LOCAL
1490 m_scrollbar_buf.draw();
1491 #endif
1492 }
1493 }
1494
_get_preferred_size(Direction dim,int prosp_width)1495 SizeReq Scroller::_get_preferred_size(Direction dim, int prosp_width)
1496 {
1497 if (!m_child)
1498 return { 0, 0 };
1499
1500 SizeReq sr = m_child->get_preferred_size(dim, prosp_width);
1501 if (dim) sr.min = 0; // can shrink to zero height
1502 return sr;
1503 }
1504
_allocate_region()1505 void Scroller::_allocate_region()
1506 {
1507 SizeReq sr = m_child->get_preferred_size(Widget::VERT, m_region.width);
1508 m_scroll = max(0, min(m_scroll, sr.nat-m_region.height));
1509 Region ch_reg = {m_region.x, m_region.y-m_scroll, m_region.width, sr.nat};
1510 m_child->allocate_region(ch_reg);
1511
1512 #ifdef USE_TILE_LOCAL
1513 int shade_height = UI_SCROLLER_SHADE_SIZE, ds = 4;
1514 int shade_top = min({m_scroll/ds, shade_height, m_region.height/2});
1515 int shade_bot = min({(sr.nat-m_region.height-m_scroll)/ds, shade_height, m_region.height/2});
1516 const VColour col_a(4, 2, 4, 0), col_b(4, 2, 4, 200);
1517
1518 m_shade_buf.clear();
1519 m_scrollbar_buf.clear();
1520 {
1521 GLWPrim rect(m_region.x, m_region.y+shade_top-shade_height,
1522 m_region.x+m_region.width, m_region.y+shade_top);
1523 rect.set_col(col_b, col_a);
1524 m_shade_buf.add_primitive(rect);
1525 }
1526 {
1527 GLWPrim rect(m_region.x, m_region.y+m_region.height-shade_bot,
1528 m_region.x+m_region.width, m_region.y+m_region.height-shade_bot+shade_height);
1529 rect.set_col(col_a, col_b);
1530 m_shade_buf.add_primitive(rect);
1531 }
1532 if (ch_reg.height > m_region.height && m_scrolbar_visible)
1533 {
1534 const int x = m_region.x+m_region.width;
1535 const float h_percent = m_region.height / (float)ch_reg.height;
1536 const int h = m_region.height*min(max(0.05f, h_percent), 1.0f);
1537 const float scroll_percent = m_scroll/(float)(ch_reg.height-m_region.height);
1538 const int y = m_region.y + (m_region.height-h)*scroll_percent;
1539 GLWPrim bg_rect(x+10, m_region.y, x+12, m_region.y+m_region.height);
1540 bg_rect.set_col(VColour(41, 41, 41));
1541 m_scrollbar_buf.add_primitive(bg_rect);
1542 GLWPrim fg_rect(x+10, y, x+12, y+h);
1543 fg_rect.set_col(VColour(125, 98, 60));
1544 m_scrollbar_buf.add_primitive(fg_rect);
1545 }
1546 #endif
1547 }
1548
on_event(const Event & event)1549 bool Scroller::on_event(const Event& event)
1550 {
1551 if (Bin::on_event(event))
1552 return true;
1553 #ifdef USE_TILE_LOCAL
1554 const int line_delta = 20;
1555 #else
1556 const int line_delta = 1;
1557 #endif
1558 int delta = 0;
1559 if (event.type() == Event::Type::KeyDown)
1560 {
1561 const auto key = static_cast<const KeyEvent&>(event).key();
1562 switch (key)
1563 {
1564 case ' ': case '+': case CK_PGDN: case '>': case '\'':
1565 delta = m_region.height;
1566 break;
1567
1568 case '-': case CK_PGUP: case '<': case ';':
1569 delta = -m_region.height;
1570 break;
1571
1572 case CK_UP:
1573 delta = -line_delta;
1574 break;
1575
1576 case CK_DOWN:
1577 case CK_ENTER:
1578 delta = line_delta;
1579 break;
1580
1581 case CK_HOME:
1582 set_scroll(0);
1583 return true;
1584
1585 case CK_END:
1586 set_scroll(INT_MAX);
1587 return true;
1588 }
1589 }
1590 else if (event.type() == Event::Type::MouseWheel)
1591 {
1592 auto mouse_event = static_cast<const MouseEvent&>(event);
1593 delta = -1 * mouse_event.wheel_dy() * line_delta;
1594 }
1595 else if (event.type() == Event::Type::MouseDown
1596 && static_cast<const MouseEvent&>(event).button() == MouseEvent::Button::Left)
1597 delta = line_delta;
1598 if (delta != 0)
1599 {
1600 set_scroll(m_scroll+delta);
1601 return true;
1602 }
1603 return false;
1604 }
1605
Layout(shared_ptr<Widget> child)1606 Layout::Layout(shared_ptr<Widget> child)
1607 {
1608 #ifdef USE_TILE_LOCAL
1609 m_depth = ui_root.num_children();
1610 #endif
1611 child->_set_parent(this);
1612 m_child = move(child);
1613 expand_h = expand_v = true;
1614 }
1615
_render()1616 void Layout::_render()
1617 {
1618 m_child->render();
1619 }
1620
_get_preferred_size(Direction dim,int prosp_width)1621 SizeReq Layout::_get_preferred_size(Direction dim, int prosp_width)
1622 {
1623 return m_child->get_preferred_size(dim, prosp_width);
1624 }
1625
_allocate_region()1626 void Layout::_allocate_region()
1627 {
1628 m_child->allocate_region(m_region);
1629 }
1630
_render()1631 void Popup::_render()
1632 {
1633 #ifdef USE_TILE_LOCAL
1634 m_buf.draw();
1635 #endif
1636 m_child->render();
1637 }
1638
_get_preferred_size(Direction dim,int prosp_width)1639 SizeReq Popup::_get_preferred_size(Direction dim, int prosp_width)
1640 {
1641 #ifdef USE_TILE_LOCAL
1642 // can be called with a prosp_width that is narrower than the child's
1643 // minimum width, since our returned SizeReq has a minimum of 0
1644 if (dim == VERT)
1645 {
1646 SizeReq hsr = m_child->get_preferred_size(HORZ, -1);
1647 prosp_width = max(prosp_width, hsr.min);
1648 }
1649 #endif
1650 SizeReq sr = m_child->get_preferred_size(dim, prosp_width);
1651 #ifdef USE_TILE_LOCAL
1652 const int pad = base_margin() + m_padding;
1653 return {
1654 0, sr.nat + 2*pad + (dim ? m_depth*m_depth_indent*(!m_centred) : 0)
1655 };
1656 #else
1657 return { sr.min, sr.nat };
1658 #endif
1659 }
1660
_allocate_region()1661 void Popup::_allocate_region()
1662 {
1663 Region region = m_region;
1664 #ifdef USE_TILE_LOCAL
1665 m_buf.clear();
1666 m_buf.add(m_region.x, m_region.y,
1667 m_region.x + m_region.width, m_region.y + m_region.height,
1668 VColour(0, 0, 0, 150));
1669 const int pad = base_margin() + m_padding;
1670 region.width -= 2*pad;
1671 region.height -= 2*pad + m_depth*m_depth_indent*(!m_centred);
1672
1673 SizeReq hsr = m_child->get_preferred_size(HORZ, -1);
1674 region.width = max(hsr.min, min(region.width, hsr.nat));
1675 SizeReq vsr = m_child->get_preferred_size(VERT, region.width);
1676 region.height = max(vsr.min, min(region.height, vsr.nat));
1677
1678 region.x += pad + (m_region.width-2*pad-region.width)/2;
1679 region.y += pad + (m_centred ? (m_region.height-2*pad-region.height)/2
1680 : m_depth*m_depth_indent);
1681
1682 m_buf.add(region.x - m_padding, region.y - m_padding,
1683 region.x + region.width + m_padding,
1684 region.y + region.height + m_padding,
1685 VColour(125, 98, 60));
1686 m_buf.add(region.x - m_padding + 2, region.y - m_padding + 2,
1687 region.x + region.width + m_padding - 2,
1688 region.y + region.height + m_padding - 2,
1689 VColour(0, 0, 0));
1690 m_buf.add(region.x - m_padding + 3, region.y - m_padding + 3,
1691 region.x + region.width + m_padding - 3,
1692 region.y + region.height + m_padding - 3,
1693 VColour(4, 2, 4));
1694 #else
1695 SizeReq hsr = m_child->get_preferred_size(HORZ, -1);
1696 region.width = max(hsr.min, min(region.width, hsr.nat));
1697 SizeReq vsr = m_child->get_preferred_size(VERT, region.width);
1698 region.height = max(vsr.min, min(region.height, vsr.nat));
1699 #endif
1700 m_child->allocate_region(region);
1701 }
1702
get_max_child_size()1703 Size Popup::get_max_child_size()
1704 {
1705 Size max_child_size = Size(m_region.width, m_region.height);
1706 #ifdef USE_TILE_LOCAL
1707 const int pad = base_margin() + m_padding;
1708 max_child_size.width = (max_child_size.width - 2*pad) & ~0x1;
1709 max_child_size.height = (max_child_size.height - 2*pad - m_depth*m_depth_indent) & ~0x1;
1710 #endif
1711 return max_child_size;
1712 }
1713
1714 #ifdef USE_TILE_LOCAL
base_margin()1715 int Popup::base_margin()
1716 {
1717 const int screen_small = 800, screen_large = 1000;
1718 const int margin_small = 10, margin_large = 50;
1719 const int clipped = max(screen_small, min(screen_large, m_region.height));
1720 return margin_small + (clipped-screen_small)
1721 *(margin_large-margin_small)/(screen_large-screen_small);
1722 }
1723 #endif
1724
_render()1725 void Checkbox::_render()
1726 {
1727 if (m_child)
1728 m_child->render();
1729
1730 const bool has_focus = ui::get_focused_widget() == this;
1731
1732 #ifdef USE_TILE_LOCAL
1733 tileidx_t tile = TILEG_CHECKBOX;
1734 if (m_checked)
1735 tile += 1;
1736 if (m_hovered || has_focus)
1737 tile += 2;
1738
1739 const int x = m_region.x, y = m_region.y;
1740 TileBuffer tb;
1741 tb.set_tex(&tiles.get_image_manager()->m_textures[TEX_GUI]);
1742 tb.add(tile, x, y, 0, 0, false, check_h, 1.0, 1.0);
1743 tb.draw();
1744 #else
1745 cgotoxy(m_region.x+1, m_region.y+1, GOTO_CRT);
1746 textbackground(has_focus ? LIGHTGREY : BLACK);
1747 cprintf("[ ]");
1748 if (m_checked)
1749 {
1750 cgotoxy(m_region.x+2, m_region.y+1, GOTO_CRT);
1751 textcolour(has_focus ? BLACK : WHITE);
1752 cprintf("X");
1753 }
1754 #endif
1755 }
1756
1757 const int Checkbox::check_w;
1758 const int Checkbox::check_h;
1759
_get_preferred_size(Direction dim,int prosp_width)1760 SizeReq Checkbox::_get_preferred_size(Direction dim, int prosp_width)
1761 {
1762 SizeReq child_sr = { 0, 0 };
1763 if (m_child)
1764 child_sr = m_child->get_preferred_size(dim, prosp_width);
1765
1766 if (dim == HORZ)
1767 return { child_sr.min + check_w, child_sr.nat + check_w };
1768 else
1769 return { max(child_sr.min, check_h), max(child_sr.nat, check_h) };
1770 }
1771
_allocate_region()1772 void Checkbox::_allocate_region()
1773 {
1774 if (m_child)
1775 {
1776 auto child_region = m_region;
1777 child_region.x += check_w;
1778 child_region.width -= check_w;
1779 auto child_sr = m_child->get_preferred_size(VERT, child_region.width);
1780 child_region.height = min(max(child_sr.min, m_region.height), child_sr.nat);
1781 child_region.y += (m_region.height - child_region.height)/2;
1782 m_child->allocate_region(child_region);
1783 }
1784 }
1785
on_event(const Event & event)1786 bool Checkbox::on_event(const Event& event)
1787 {
1788 #ifdef USE_TILE_LOCAL
1789 if (event.type() == Event::Type::MouseEnter || event.type() == Event::Type::MouseLeave)
1790 {
1791 bool new_hovered = event.type() == Event::Type::MouseEnter;
1792 if (new_hovered != m_hovered)
1793 _expose();
1794 m_hovered = new_hovered;
1795 }
1796 if (event.type() == Event::Type::MouseDown)
1797 {
1798 set_checked(!checked());
1799 set_focused_widget(this);
1800 _expose();
1801 return true;
1802 }
1803 #endif
1804 if (event.type() == Event::Type::FocusIn || event.type() == Event::Type::FocusOut)
1805 {
1806 _expose();
1807 return true;
1808 }
1809 if (event.type() == Event::Type::KeyDown)
1810 {
1811 const auto key = static_cast<const KeyEvent&>(event).key();
1812 if (key == CK_ENTER || key == ' ')
1813 {
1814 set_checked(!checked());
1815 _expose();
1816 return true;
1817 }
1818 }
1819 return false;
1820 }
1821
1822 #ifdef USE_TILE_WEB
sync_save_state()1823 void Checkbox::sync_save_state()
1824 {
1825 tiles.json_write_bool("checked", m_checked);
1826 }
1827
sync_load_state(const JsonNode * json)1828 void Checkbox::sync_load_state(const JsonNode *json)
1829 {
1830 if (auto c = json_find_member(json, "checked"))
1831 if (c->tag == JSON_BOOL)
1832 set_checked(c->bool_);
1833 }
1834 #endif
1835
TextEntry()1836 TextEntry::TextEntry() : m_line_reader(m_buffer, sizeof(m_buffer))
1837 {
1838 #ifdef USE_TILE_LOCAL
1839 set_font(tiles.get_crt_font());
1840 #endif
1841 }
1842
_render()1843 void TextEntry::_render()
1844 {
1845 const bool has_focus = ui::get_focused_widget() == this;
1846
1847 #ifdef USE_TILE_LOCAL
1848 const int line_height = m_font->char_height();
1849 const int text_y = m_region.y + (m_region.height - line_height)/2;
1850
1851 const auto bg = has_focus ? VColour(30, 30, 30, 255)
1852 : VColour(29, 27, 21, 255);
1853
1854 m_buf.clear();
1855 m_buf.add(m_region.x, m_region.y, m_region.ex(), m_region.ey(), bg);
1856 m_buf.draw();
1857
1858 const auto border_bg = has_focus ? VColour(184, 141, 25)
1859 : VColour(125, 98, 60);
1860
1861 LineBuffer bbuf;
1862 bbuf.add_square(m_region.x+1, m_region.y+1,
1863 m_region.ex(), m_region.ey(), border_bg);
1864 bbuf.draw();
1865
1866 const int content_width = m_font->string_width(m_text.c_str());
1867 const int cursor_x = m_font->string_width(
1868 m_text.substr(0, m_cursor).c_str());
1869 constexpr int x_pad = 3;
1870 #else
1871 const int content_width = strwidth(m_text);
1872 const int cursor_x = strwidth(m_text.substr(0, m_cursor));
1873 constexpr int x_pad = 0;
1874 #endif
1875
1876 const int viewport_width = m_region.width - 2*x_pad;
1877
1878 // Scroll to keep the cursor in view
1879 if (cursor_x < m_hscroll)
1880 m_hscroll = cursor_x;
1881 else if (cursor_x >= m_hscroll + viewport_width)
1882 m_hscroll = cursor_x - viewport_width + 1;
1883
1884 // scroll to keep the textbox full of text, if possible
1885 m_hscroll = min(m_hscroll, max(0, content_width - viewport_width + 1));
1886
1887 #ifdef USE_TILE_LOCAL
1888 // XXX: we need to transform the scissor because the skill menu is rendered
1889 // using the CRT, with an appropriate transform that positions it into the
1890 // centre of the screen
1891 GLW_3VF translate;
1892 glmanager->get_transform(&translate, nullptr);
1893 const Region scissor_region = {
1894 m_region.x - static_cast<int>(translate.x),
1895 m_region.y - static_cast<int>(translate.y),
1896 m_region.width, m_region.height,
1897 };
1898 scissor_stack.push(scissor_region);
1899
1900 const int text_x = m_region.x - m_hscroll + x_pad;
1901
1902 FontBuffer m_font_buf(m_font);
1903 m_font_buf.add(formatted_string(m_text), text_x, text_y);
1904 m_font_buf.draw();
1905
1906 scissor_stack.pop();
1907
1908 if (has_focus)
1909 {
1910 m_buf.clear();
1911 m_buf.add(text_x + cursor_x, text_y,
1912 text_x + cursor_x + 1, text_y + line_height,
1913 VColour(255, 255, 255, 255));
1914 m_buf.draw();
1915 }
1916 #else
1917 auto prefix_size = chop_string(m_text, m_hscroll, false).size();
1918 auto remain = chop_string(m_text.substr(prefix_size), m_region.width, true);
1919
1920 const auto fg_colour = has_focus ? BLACK : WHITE;
1921 const auto bg_colour = has_focus ? LIGHTGREY : DARKGREY;
1922
1923 draw_colour draw(fg_colour, bg_colour);
1924 cgotoxy(m_region.x+1, m_region.y+1, GOTO_CRT);
1925 cprintf("%s", remain.c_str());
1926
1927 if (has_focus)
1928 {
1929 cgotoxy(m_region.x+cursor_x-m_hscroll+1, m_region.y+1, GOTO_CRT);
1930 textcolour(DARKGREY);
1931 cprintf(" ");
1932 show_cursor_at(m_region.x+cursor_x-m_hscroll+1, m_region.y+1);
1933 }
1934 #endif
1935 }
1936
_get_preferred_size(Direction dim,int)1937 SizeReq TextEntry::_get_preferred_size(Direction dim, int /*prosp_width*/)
1938 {
1939 if (!dim)
1940 return { 0, 300 };
1941 else
1942 {
1943 #ifdef USE_TILE_LOCAL
1944 const int line_height = m_font->char_height(false);
1945 const int height = line_height + 2*padding_size();
1946 return { height, height };
1947 #else
1948 return { 1, 1 };
1949 #endif
1950 }
1951 }
1952
1953 #ifdef USE_TILE_LOCAL
padding_size()1954 int TextEntry::padding_size()
1955 {
1956 const int line_height = m_font->char_height(false);
1957 const float pad_amount = 0.2;
1958 return (static_cast<int>(line_height*pad_amount) + 1)/2;
1959 }
1960 #endif
1961
_allocate_region()1962 void TextEntry::_allocate_region()
1963 {
1964 }
1965
on_event(const Event & event)1966 bool TextEntry::on_event(const Event& event)
1967 {
1968 switch (event.type())
1969 {
1970 case Event::Type::FocusIn:
1971 case Event::Type::FocusOut:
1972 set_cursor_enabled(event.type() == Event::Type::FocusIn);
1973 _expose();
1974 return true;
1975 case Event::Type::MouseDown:
1976 // TODO: unfocus if the mouse is clicked outside
1977 // TODO: move the cursor to the clicked position
1978 if (static_cast<const MouseEvent&>(event).button() == MouseEvent::Button::Left)
1979 ui::set_focused_widget(this);
1980 return true;
1981 case Event::Type::KeyDown:
1982 {
1983 const auto key = static_cast<const KeyEvent&>(event).key();
1984 int ret = m_line_reader.process_key_core(key);
1985 if (ret == CK_ESCAPE || ret == 0)
1986 ui::set_focused_widget(nullptr);
1987 m_text = m_line_reader.get_text();
1988 m_cursor = m_line_reader.get_cursor_position();
1989 _expose();
1990 return key != '\t' && key != CK_SHIFT_TAB;
1991 }
1992 default:
1993 return false;
1994 }
1995 }
1996
1997 #ifdef USE_TILE_LOCAL
set_font(FontWrapper * font)1998 void TextEntry::set_font(FontWrapper *font)
1999 {
2000 ASSERT(font);
2001 m_font = font;
2002 _invalidate_sizereq();
2003 }
2004 #endif
2005
LineReader(char * buf,size_t sz)2006 TextEntry::LineReader::LineReader(char *buf, size_t sz)
2007 : buffer(buf), bufsz(sz), history(nullptr), keyfn(nullptr),
2008 mode(EDIT_MODE_INSERT),
2009 cur(nullptr), length(0)
2010 {
2011 *buffer = 0;
2012 length = 0;
2013 cur = buffer;
2014
2015 if (history)
2016 history->go_end();
2017 }
2018
~LineReader()2019 TextEntry::LineReader::~LineReader()
2020 {
2021 }
2022
get_text() const2023 string TextEntry::LineReader::get_text() const
2024 {
2025 return buffer;
2026 }
2027
set_text(string text)2028 void TextEntry::LineReader::set_text(string text)
2029 {
2030 snprintf(buffer, bufsz, "%s", text.c_str());
2031 length = min(text.size(), bufsz - 1);
2032 buffer[length] = 0;
2033 cur = buffer + length;
2034 }
2035
set_input_history(input_history * i)2036 void TextEntry::LineReader::set_input_history(input_history *i)
2037 {
2038 history = i;
2039 }
2040
set_keyproc(keyproc fn)2041 void TextEntry::LineReader::set_keyproc(keyproc fn)
2042 {
2043 keyfn = fn;
2044 }
2045
set_edit_mode(edit_mode m)2046 void TextEntry::LineReader::set_edit_mode(edit_mode m)
2047 {
2048 mode = m;
2049 }
2050
set_prompt(string p)2051 void TextEntry::LineReader::set_prompt(string p)
2052 {
2053 prompt = p;
2054 }
2055
get_edit_mode()2056 edit_mode TextEntry::LineReader::get_edit_mode()
2057 {
2058 return mode;
2059 }
2060
2061 #ifdef USE_TILE_WEB
set_tag(const string & id)2062 void TextEntry::LineReader::set_tag(const string &id)
2063 {
2064 tag = id;
2065 }
2066 #endif
2067
process_key_core(int ch)2068 int TextEntry::LineReader::process_key_core(int ch)
2069 {
2070 if (keyfn)
2071 {
2072 // if you intercept esc, don't forget to provide another way to
2073 // exit. Processing esc will safely cancel.
2074 keyfun_action whattodo = (*keyfn)(ch);
2075 if (whattodo == KEYFUN_CLEAR)
2076 {
2077 buffer[length] = 0;
2078 if (history && length)
2079 history->new_input(buffer);
2080 return 0;
2081 }
2082 else if (whattodo == KEYFUN_BREAK)
2083 {
2084 buffer[length] = 0;
2085 return ch;
2086 }
2087 else if (whattodo == KEYFUN_IGNORE)
2088 return -1;
2089 // else case: KEYFUN_PROCESS
2090 }
2091
2092 return process_key(ch);
2093 }
2094
backspace()2095 void TextEntry::LineReader::backspace()
2096 {
2097 char *np = prev_glyph(cur, buffer);
2098 if (!np)
2099 return;
2100 char32_t ch;
2101 utf8towc(&ch, np);
2102 buffer[length] = 0;
2103 length -= cur - np;
2104 char *c = cur;
2105 cur = np;
2106 while (*c)
2107 *np++ = *c++;
2108 buffer[length] = 0;
2109 }
2110
delete_char()2111 void TextEntry::LineReader::delete_char()
2112 {
2113 // TODO: unify with backspace
2114 if (*cur)
2115 {
2116 const char *np = next_glyph(cur);
2117 ASSERT(np);
2118 char32_t ch_at_point;
2119 utf8towc(&ch_at_point, cur);
2120 const size_t del_bytes = np - cur;
2121 const size_t follow_bytes = (buffer + length) - np;
2122 // Copy the NUL too.
2123 memmove(cur, np, follow_bytes + 1);
2124 length -= del_bytes;
2125 }
2126 }
2127
is_wordchar(char32_t c)2128 bool TextEntry::LineReader::is_wordchar(char32_t c)
2129 {
2130 return iswalnum(c) || c == '_' || c == '-';
2131 }
2132
kill_to_begin()2133 void TextEntry::LineReader::kill_to_begin()
2134 {
2135 if (cur == buffer)
2136 return;
2137
2138 const int rest = length - (cur - buffer);
2139 memmove(buffer, cur, rest);
2140 length = rest;
2141 buffer[length] = 0;
2142 cur = buffer;
2143 }
2144
kill_to_end()2145 void TextEntry::LineReader::kill_to_end()
2146 {
2147 if (*cur)
2148 {
2149 length = cur - buffer;
2150 *cur = 0;
2151 }
2152 }
2153
killword()2154 void TextEntry::LineReader::killword()
2155 {
2156 if (cur == buffer)
2157 return;
2158
2159 bool foundwc = false;
2160 char *word = cur;
2161 int ew = 0;
2162 while (1)
2163 {
2164 char *np = prev_glyph(word, buffer);
2165 if (!np)
2166 break;
2167
2168 char32_t c;
2169 utf8towc(&c, np);
2170 if (is_wordchar(c))
2171 foundwc = true;
2172 else if (foundwc)
2173 break;
2174
2175 word = np;
2176 ew += wcwidth(c);
2177 }
2178 memmove(word, cur, strlen(cur) + 1);
2179 length -= cur - word;
2180 cur = word;
2181 }
2182
overwrite_char_at_cursor(int ch)2183 void TextEntry::LineReader::overwrite_char_at_cursor(int ch)
2184 {
2185 int len = wclen(ch);
2186 int w = wcwidth(ch);
2187
2188 if (w >= 0 && cur - buffer + len < static_cast<int>(bufsz))
2189 {
2190 bool empty = !*cur;
2191
2192 wctoutf8(cur, ch);
2193 cur += len;
2194 if (empty)
2195 length += len;
2196 buffer[length] = 0;
2197 }
2198 }
2199
insert_char_at_cursor(int ch)2200 void TextEntry::LineReader::insert_char_at_cursor(int ch)
2201 {
2202 if (wcwidth(ch) >= 0 && length + wclen(ch) < static_cast<int>(bufsz))
2203 {
2204 int len = wclen(ch);
2205 if (*cur)
2206 {
2207 char *c = buffer + length - 1;
2208 while (c >= cur)
2209 {
2210 c[len] = *c;
2211 c--;
2212 }
2213 }
2214 wctoutf8(cur, ch);
2215 cur += len;
2216 length += len;
2217 buffer[length] = 0;
2218 }
2219 }
2220
2221 #ifdef USE_TILE_LOCAL
clipboard_paste()2222 void TextEntry::LineReader::clipboard_paste()
2223 {
2224 if (wm->has_clipboard())
2225 for (char ch : wm->get_clipboard())
2226 process_key(ch);
2227 }
2228 #endif
2229
process_key(int ch)2230 int TextEntry::LineReader::process_key(int ch)
2231 {
2232 switch (ch)
2233 {
2234 CASE_ESCAPE
2235 return CK_ESCAPE;
2236 case CK_UP:
2237 case CONTROL('P'):
2238 case CK_DOWN:
2239 case CONTROL('N'):
2240 {
2241 if (!history)
2242 break;
2243
2244 const string *text = (ch == CK_UP || ch == CONTROL('P'))
2245 ? history->prev()
2246 : history->next();
2247
2248 if (text)
2249 set_text(*text);
2250 break;
2251 }
2252 case CK_ENTER:
2253 buffer[length] = 0;
2254 if (history && length)
2255 history->new_input(buffer);
2256 return 0;
2257
2258 case CONTROL('K'):
2259 kill_to_end();
2260 break;
2261
2262 case CK_DELETE:
2263 case CONTROL('D'):
2264 delete_char();
2265 break;
2266
2267 case CK_BKSP:
2268 backspace();
2269 break;
2270
2271 case CONTROL('W'):
2272 killword();
2273 break;
2274
2275 case CONTROL('U'):
2276 kill_to_begin();
2277 break;
2278
2279 case CK_LEFT:
2280 case CONTROL('B'):
2281 if (char *np = prev_glyph(cur, buffer))
2282 cur = np;
2283 break;
2284 case CK_RIGHT:
2285 case CONTROL('F'):
2286 if (char *np = next_glyph(cur))
2287 cur = np;
2288 break;
2289 case CK_HOME:
2290 case CONTROL('A'):
2291 cur = buffer;
2292 break;
2293 case CK_END:
2294 case CONTROL('E'):
2295 cur = buffer + length;
2296 break;
2297 case CONTROL('V'):
2298 #ifdef USE_TILE_LOCAL
2299 clipboard_paste();
2300 #endif
2301 break;
2302 case CK_REDRAW:
2303 //redraw_screen();
2304 return -1;
2305 default:
2306 if (mode == EDIT_MODE_OVERWRITE)
2307 overwrite_char_at_cursor(ch);
2308 else // mode == EDIT_MODE_INSERT
2309 insert_char_at_cursor(ch);
2310
2311 break;
2312 }
2313
2314 return -1;
2315 }
2316
2317 #ifdef USE_TILE_WEB
sync_save_state()2318 void TextEntry::sync_save_state()
2319 {
2320 tiles.json_write_string("text", m_text);
2321 tiles.json_write_int("cursor", m_cursor);
2322 }
2323
sync_load_state(const JsonNode * json)2324 void TextEntry::sync_load_state(const JsonNode *json)
2325 {
2326 if (auto text = json_find_member(json, "text"))
2327 if (text->tag == JSON_STRING)
2328 set_text(text->string_);
2329
2330 // TODO: sync cursor state
2331 }
2332 #endif
2333
2334 #ifdef USE_TILE_LOCAL
_render()2335 void Dungeon::_render()
2336 {
2337 GLW_3VF t = {(float)m_region.x, (float)m_region.y, 0}, s = {32, 32, 1};
2338 glmanager->set_transform(t, s);
2339 m_buf.draw();
2340 glmanager->reset_transform();
2341 }
2342
_get_preferred_size(Direction dim,int)2343 SizeReq Dungeon::_get_preferred_size(Direction dim, int /*prosp_width*/)
2344 {
2345 const int sz = (dim ? height : width)*32;
2346 return { sz, sz };
2347 }
2348
PlayerDoll(dolls_data doll)2349 PlayerDoll::PlayerDoll(dolls_data doll)
2350 {
2351 m_save_doll = doll;
2352 for (int i = 0; i < TEX_MAX; i++)
2353 m_tile_buf[i].set_tex(&tiles.get_image_manager()->m_textures[i]);
2354 _pack_doll();
2355 }
2356
~PlayerDoll()2357 PlayerDoll::~PlayerDoll()
2358 {
2359 for (int t = 0; t < TEX_MAX; t++)
2360 m_tile_buf[t].clear();
2361 }
2362
_pack_doll()2363 void PlayerDoll::_pack_doll()
2364 {
2365 m_tiles.clear();
2366 // FIXME: Implement this logic in one place in e.g. pack_doll_buf().
2367 int p_order[TILEP_PART_MAX] =
2368 {
2369 TILEP_PART_SHADOW, // 0
2370 TILEP_PART_HALO,
2371 TILEP_PART_ENCH,
2372 TILEP_PART_DRCWING,
2373 TILEP_PART_CLOAK,
2374 TILEP_PART_BASE, // 5
2375 TILEP_PART_BOOTS,
2376 TILEP_PART_LEG,
2377 TILEP_PART_BODY,
2378 TILEP_PART_ARM,
2379 TILEP_PART_HAIR,
2380 TILEP_PART_BEARD,
2381 TILEP_PART_DRCHEAD, // 15
2382 TILEP_PART_HELM,
2383 TILEP_PART_HAND1, // 10
2384 TILEP_PART_HAND2,
2385 };
2386
2387 int flags[TILEP_PART_MAX];
2388 tilep_calc_flags(m_save_doll, flags);
2389
2390 // For skirts, boots go under the leg armour. For pants, they go over.
2391 if (m_save_doll.parts[TILEP_PART_LEG] < TILEP_LEG_SKIRT_OFS)
2392 {
2393 p_order[6] = TILEP_PART_BOOTS;
2394 p_order[7] = TILEP_PART_LEG;
2395 }
2396
2397 // Special case bardings from being cut off.
2398 bool is_naga = (m_save_doll.parts[TILEP_PART_BASE] == TILEP_BASE_NAGA
2399 || m_save_doll.parts[TILEP_PART_BASE] == TILEP_BASE_NAGA + 1);
2400 if (m_save_doll.parts[TILEP_PART_BOOTS] >= TILEP_BOOTS_NAGA_BARDING
2401 && m_save_doll.parts[TILEP_PART_BOOTS] <= TILEP_BOOTS_NAGA_BARDING_RED)
2402 {
2403 flags[TILEP_PART_BOOTS] = is_naga ? TILEP_FLAG_NORMAL : TILEP_FLAG_HIDE;
2404 }
2405
2406 bool is_ptng = (m_save_doll.parts[TILEP_PART_BASE] == TILEP_BASE_PALENTONGA
2407 || m_save_doll.parts[TILEP_PART_BASE] == TILEP_BASE_PALENTONGA + 1);
2408 if (m_save_doll.parts[TILEP_PART_BOOTS] >= TILEP_BOOTS_CENTAUR_BARDING
2409 && m_save_doll.parts[TILEP_PART_BOOTS] <= TILEP_BOOTS_CENTAUR_BARDING_RED)
2410 {
2411 flags[TILEP_PART_BOOTS] = is_ptng ? TILEP_FLAG_NORMAL : TILEP_FLAG_HIDE;
2412 }
2413
2414 for (int i = 0; i < TILEP_PART_MAX; ++i)
2415 {
2416 const int p = p_order[i];
2417 const tileidx_t idx = m_save_doll.parts[p];
2418 if (idx == 0 || idx == TILEP_SHOW_EQUIP || flags[p] == TILEP_FLAG_HIDE)
2419 continue;
2420
2421 ASSERT_RANGE(idx, TILE_MAIN_MAX, TILEP_PLAYER_MAX);
2422
2423 int ymax = TILE_Y;
2424
2425 if (flags[p] == TILEP_FLAG_CUT_CENTAUR
2426 || flags[p] == TILEP_FLAG_CUT_NAGA)
2427 {
2428 ymax = 18;
2429 }
2430
2431 m_tiles.emplace_back(idx, ymax);
2432 }
2433 }
2434
_render()2435 void PlayerDoll::_render()
2436 {
2437 for (int i = 0; i < TEX_MAX; i++)
2438 m_tile_buf[i].draw();
2439 }
2440
_get_preferred_size(Direction,int)2441 SizeReq PlayerDoll::_get_preferred_size(Direction /*dim*/, int /*prosp_width*/)
2442 {
2443 return { TILE_Y, TILE_Y };
2444 }
2445
_allocate_region()2446 void PlayerDoll::_allocate_region()
2447 {
2448 for (int t = 0; t < TEX_MAX; t++)
2449 m_tile_buf[t].clear();
2450 for (const tile_def &tdef : m_tiles)
2451 {
2452 int tile = tdef.tile;
2453 TextureID tex = get_tile_texture(tile);
2454 m_tile_buf[tex].add_unscaled(tile, m_region.x, m_region.y, tdef.ymax);
2455 }
2456 }
2457 #endif
2458
push_child(shared_ptr<Widget> ch,KeymapContext km)2459 void UIRoot::push_child(shared_ptr<Widget> ch, KeymapContext km)
2460 {
2461 Widget* focus;
2462 if (auto popup = dynamic_cast<Popup*>(ch.get()))
2463 focus = popup->get_child().get();
2464 else
2465 focus = ch.get();
2466
2467 saved_layout_info.push_back(state);
2468 state.keymap = km;
2469 state.default_focus = state.current_focus = focus;
2470 state.generation_id = next_generation_id++;
2471
2472 m_root.add_child(move(ch));
2473 m_needs_layout = true;
2474 m_changed_layout_since_click = true;
2475 update_focus_order();
2476 #ifdef USE_TILE_WEB
2477 update_synced_widgets();
2478 #endif
2479 #ifndef USE_TILE_LOCAL
2480 if (m_root.num_children() == 1)
2481 {
2482 clrscr();
2483 ui_root.resize(get_number_of_cols(), get_number_of_lines());
2484 }
2485 #endif
2486 }
2487
pop_child()2488 void UIRoot::pop_child()
2489 {
2490 top_child()->_emit_layout_pop();
2491 m_root.pop_child();
2492 m_needs_layout = true;
2493 state = saved_layout_info.back();
2494 saved_layout_info.pop_back();
2495 m_changed_layout_since_click = true;
2496 update_focus_order();
2497 #ifdef USE_TILE_WEB
2498 update_synced_widgets();
2499 #endif
2500 #ifndef USE_TILE_LOCAL
2501 if (m_root.num_children() == 0)
2502 clrscr();
2503 #endif
2504 }
2505
resize(int w,int h)2506 void UIRoot::resize(int w, int h)
2507 {
2508 if (w == m_w && h == m_h)
2509 return;
2510
2511 m_w = w;
2512 m_h = h;
2513 m_needs_layout = true;
2514
2515 // On console with the window size smaller than the minimum layout,
2516 // enlarging the window will not cause any size reallocations, and the
2517 // newly visible region of the terminal will not be filled.
2518 // Fix: explicitly mark the entire screen as dirty on resize: it won't
2519 // be strictly necessary for most resizes, but won't hurt.
2520 #ifndef USE_TILE_LOCAL
2521 expose_region({0, 0, w, h});
2522 #endif
2523 }
2524
layout()2525 void UIRoot::layout()
2526 {
2527 while (m_needs_layout)
2528 {
2529 m_needs_layout = false;
2530
2531 // Find preferred size with height-for-width: we never allocate less than
2532 // the minimum size, but may allocate more than the natural size.
2533 SizeReq sr_horz = m_root.get_preferred_size(Widget::HORZ, -1);
2534 int width = max(sr_horz.min, m_w);
2535 SizeReq sr_vert = m_root.get_preferred_size(Widget::VERT, width);
2536 int height = max(sr_vert.min, m_h);
2537
2538 #ifdef USE_TILE_LOCAL
2539 m_region = {0, 0, width, height};
2540 #else
2541 m_region = {0, 0, m_w, m_h};
2542 #endif
2543 try
2544 {
2545 m_root.allocate_region({0, 0, width, height});
2546 }
2547 catch (const RestartAllocation &ex)
2548 {
2549 }
2550
2551 #ifdef USE_TILE_LOCAL
2552 update_hover_path();
2553 #endif
2554 }
2555 }
2556
2557 #ifdef USE_TILE_LOCAL
2558 bool should_render_current_regions = true;
2559 #endif
2560
render()2561 void UIRoot::render()
2562 {
2563 if (!needs_paint)
2564 return;
2565
2566 #ifdef USE_TILE_LOCAL
2567 glmanager->reset_view_for_redraw();
2568 tiles.maybe_redraw_screen();
2569 if (should_render_current_regions)
2570 tiles.render_current_regions();
2571 glmanager->reset_transform();
2572 #else
2573 // On console, clear and redraw only the dirty region of the screen
2574 m_dirty_region = m_dirty_region.aabb_intersect(m_region);
2575 textcolour(LIGHTGREY);
2576 textbackground(BLACK);
2577 clear_text_region(m_dirty_region, BLACK);
2578 #endif
2579
2580 scissor_stack.push(m_region);
2581 #ifdef USE_TILE_LOCAL
2582 int cutoff = cutoff_stack.empty() ? 0 : cutoff_stack.back();
2583 ASSERT(cutoff <= static_cast<int>(m_root.num_children()));
2584 for (int i = cutoff; i < static_cast<int>(m_root.num_children()); i++)
2585 m_root.get_child(i)->render();
2586 #ifdef DEBUG
2587 debug_render();
2588 #endif
2589 #else
2590 // Render only the top of the UI stack on console
2591 if (m_root.num_children() > 0)
2592 m_root.get_child(m_root.num_children()-1)->render();
2593 else
2594 {
2595 redraw_screen(false);
2596 update_screen();
2597 }
2598
2599 if (!cursor_pos.origin())
2600 {
2601 cursorxy(cursor_pos.x, cursor_pos.y);
2602 cursor_pos.reset();
2603 }
2604 #endif
2605 scissor_stack.pop();
2606
2607 needs_paint = false;
2608 needs_swap = true;
2609 m_dirty_region = {0, 0, 0, 0};
2610 }
2611
swap_buffers()2612 void UIRoot::swap_buffers()
2613 {
2614 if (!needs_swap)
2615 return;
2616 needs_swap = false;
2617 #ifdef USE_TILE_LOCAL
2618 wm->swap_buffers();
2619 #else
2620 update_screen();
2621 #endif
2622 }
2623
2624 #ifdef DEBUG
debug_render()2625 void UIRoot::debug_render()
2626 {
2627 #ifdef USE_TILE_LOCAL
2628 if (debug_draw)
2629 {
2630 LineBuffer lb;
2631 ShapeBuffer sb;
2632 size_t i = 0;
2633 for (const auto& w : hover_path)
2634 {
2635 const auto r = w->get_region();
2636 i++;
2637 VColour lc;
2638 lc = i == hover_path.size() ?
2639 VColour(255, 100, 0, 100) : VColour(0, 50 + i*40, 0, 100);
2640 lb.add_square(r.x+1, r.y+1, r.ex(), r.ey(), lc);
2641 }
2642 if (!hover_path.empty())
2643 {
2644 const auto& hovered_widget = hover_path.back();
2645 Region r = hovered_widget->get_region();
2646 const Margin m = hovered_widget->get_margin();
2647
2648 VColour lc = VColour(0, 0, 100, 100);
2649 sb.add(r.x, r.y-m.top, r.ex(), r.y, lc);
2650 sb.add(r.ex(), r.y, r.ex()+m.right, r.ey(), lc);
2651 sb.add(r.x, r.ey(), r.ex(), r.ey()+m.bottom, lc);
2652 sb.add(r.x-m.left, r.y, r.x, r.ey(), lc);
2653 }
2654 if (auto w = get_focused_widget())
2655 {
2656 Region r = w->get_region();
2657 lb.add_square(r.x+1, r.y+1, r.ex(), r.ey(), VColour(128, 31, 239));
2658 }
2659 lb.draw();
2660 sb.draw();
2661 }
2662 #endif
2663 }
2664 #endif
2665
update_focus_order()2666 void UIRoot::update_focus_order()
2667 {
2668 focus_order.clear();
2669
2670 function<void(Widget*)> recurse = [&](Widget* widget) {
2671 if (widget->can_take_focus())
2672 focus_order.emplace_back(widget);
2673 widget->for_each_child_including_internal([&](shared_ptr<Widget>& ch) {
2674 recurse(ch.get());
2675 });
2676 };
2677
2678 int layer_idx = m_root.num_children()-1;
2679 if (layer_idx >= 0)
2680 {
2681 auto layer_root = m_root.get_child(layer_idx).get();
2682 recurse(layer_root);
2683 }
2684 }
2685
focus_next()2686 void UIRoot::focus_next()
2687 {
2688 if (focus_order.empty())
2689 return;
2690
2691 const auto default_focus = state.default_focus;
2692 const auto current_focus = state.current_focus;
2693
2694 if (!current_focus || current_focus == default_focus)
2695 {
2696 set_focused_widget(focus_order.front());
2697 return;
2698 }
2699
2700 auto it = find(focus_order.begin(), focus_order.end(), current_focus);
2701 ASSERT(it != focus_order.end());
2702
2703 do {
2704 if (*it == focus_order.back())
2705 it = focus_order.begin();
2706 else
2707 ++it;
2708 } while (*it != current_focus && !(*it)->focusable());
2709
2710 set_focused_widget(*it);
2711 }
2712
focus_prev()2713 void UIRoot::focus_prev()
2714 {
2715 if (focus_order.empty())
2716 return;
2717
2718 const auto default_focus = state.default_focus;
2719 const auto current_focus = state.current_focus;
2720
2721 if (!current_focus || current_focus == default_focus)
2722 {
2723 set_focused_widget(focus_order.back());
2724 return;
2725 }
2726
2727 auto it = find(focus_order.begin(), focus_order.end(), current_focus);
2728 ASSERT(it != focus_order.end());
2729
2730 do {
2731 if (*it == focus_order.front())
2732 it = focus_order.end()-1;
2733 else
2734 --it;
2735 } while (*it != current_focus && !(*it)->focusable());
2736
2737 set_focused_widget(*it);
2738 }
2739
2740 static function<bool(const wm_event&)> event_filter;
2741
2742 #ifdef USE_TILE_LOCAL
update_hover_path()2743 void UIRoot::update_hover_path()
2744 {
2745 int mx, my;
2746 wm->get_mouse_state(&mx, &my);
2747
2748 /* Find current hover path */
2749 vector<Widget*> new_hover_path;
2750 shared_ptr<Widget> current = m_root.get_child_at_offset(mx, my);
2751 while (current)
2752 {
2753 new_hover_path.emplace_back(current.get());
2754 current = current->get_child_at_offset(mx, my);
2755 }
2756
2757 size_t new_hover_path_size = new_hover_path.size();
2758 size_t sz = max(hover_path.size(), new_hover_path.size());
2759 hover_path.resize(sz, nullptr);
2760 new_hover_path.resize(sz, nullptr);
2761
2762 send_mouse_enter_leave_events(hover_path, new_hover_path);
2763
2764 hover_path = move(new_hover_path);
2765 hover_path.resize(new_hover_path_size);
2766 }
2767
send_mouse_enter_leave_events(const vector<Widget * > & old_hover_path,const vector<Widget * > & new_hover_path)2768 void UIRoot::send_mouse_enter_leave_events(
2769 const vector<Widget*>& old_hover_path,
2770 const vector<Widget*>& new_hover_path)
2771 {
2772 ASSERT(old_hover_path.size() == new_hover_path.size());
2773
2774 // event_filter takes a wm_event, and we don't have one, so don't bother to
2775 // call it; this is fine, since this is a private API and it only checks for
2776 // keydown events.
2777 if (event_filter)
2778 return;
2779
2780 const size_t size = old_hover_path.size();
2781
2782 size_t diff;
2783 for (diff = 0; diff < size; diff++)
2784 if (new_hover_path[diff] != old_hover_path[diff])
2785 break;
2786
2787 if (diff == size)
2788 return;
2789
2790 if (old_hover_path[diff])
2791 {
2792 const wm_mouse_event dummy = {};
2793 MouseEvent ev(Event::Type::MouseLeave, dummy);
2794 for (size_t i = size; i > diff; --i)
2795 if (old_hover_path[i-1])
2796 old_hover_path[i-1]->on_event(ev);
2797 }
2798
2799 if (new_hover_path[diff])
2800 {
2801 const wm_mouse_event dummy = {};
2802 MouseEvent ev(Event::Type::MouseEnter, dummy);
2803 for (size_t i = diff; i < size; ++i)
2804 if (new_hover_path[i])
2805 new_hover_path[i]->on_event(ev);
2806 }
2807 }
2808
update_hover_path_for_widget(Widget * widget)2809 void UIRoot::update_hover_path_for_widget(Widget *widget)
2810 {
2811 // truncate the hover path if the widget was previously hovered,
2812 // but don't deliver any mouseleave events.
2813 if (!widget->_get_parent())
2814 {
2815 for (size_t i = 0; i < hover_path.size(); i++)
2816 if (hover_path[i] == widget)
2817 {
2818 hover_path.resize(i);
2819 break;
2820 }
2821 return;
2822 }
2823
2824 auto top = top_layout();
2825 if (top && top->is_ancestor_of(widget->get_shared()))
2826 update_hover_path();
2827 }
2828 #endif
2829
convert_event_type(const wm_event & event)2830 static Event::Type convert_event_type(const wm_event& event)
2831 {
2832 switch (event.type)
2833 {
2834 case WME_MOUSEBUTTONDOWN: return Event::Type::MouseDown;
2835 case WME_MOUSEBUTTONUP: return Event::Type::MouseUp;
2836 case WME_MOUSEMOTION: return Event::Type::MouseMove;
2837 case WME_MOUSEWHEEL: return Event::Type::MouseWheel;
2838 case WME_MOUSEENTER: return Event::Type::MouseEnter;
2839 case WME_MOUSELEAVE: return Event::Type::MouseLeave;
2840 case WME_KEYDOWN: return Event::Type::KeyDown;
2841 case WME_KEYUP: return Event::Type::KeyUp;
2842 default: abort();
2843 }
2844 }
2845
on_event(wm_event & event)2846 bool UIRoot::on_event(wm_event& event)
2847 {
2848 if (event_filter && event_filter(event))
2849 return true;
2850
2851 #ifdef DEBUG
2852 if (debug_on_event(event))
2853 return true;
2854 #endif
2855
2856 switch (event.type)
2857 {
2858 case WME_MOUSEBUTTONDOWN:
2859 case WME_MOUSEBUTTONUP:
2860 case WME_MOUSEMOTION:
2861 case WME_MOUSEWHEEL:
2862 {
2863 #ifdef USE_TILE_LOCAL
2864 auto mouse_event = MouseEvent(convert_event_type(event),
2865 event.mouse_event);
2866 return deliver_event(mouse_event);
2867 #endif
2868 break;
2869 }
2870 case WME_KEYDOWN:
2871 case WME_KEYUP:
2872 {
2873 auto key_event = KeyEvent(convert_event_type(event), event.key);
2874 return deliver_event(key_event);
2875 }
2876 case WME_CUSTOMEVENT:
2877 #ifdef USE_TILE_LOCAL
2878 {
2879 auto callback = reinterpret_cast<wm_timer_callback>(event.custom.data1);
2880 callback(0, nullptr);
2881 break;
2882 }
2883 #else
2884 break;
2885 #endif
2886 // TODO: maybe stop windowmanager-sdl from returning these?
2887 case WME_NOEVENT:
2888 break;
2889 default:
2890 die("unreachable, type %d", event.type);
2891 }
2892
2893 return false;
2894 }
2895
deliver_event(Event & event)2896 bool UIRoot::deliver_event(Event& event)
2897 {
2898 switch (event.type())
2899 {
2900 case Event::Type::MouseDown:
2901 case Event::Type::MouseUp:
2902 #ifdef USE_TILE_LOCAL
2903 if (event.type() == Event::Type::MouseDown)
2904 m_changed_layout_since_click = false;
2905 else if (event.type() == Event::Type::MouseUp)
2906 if (m_changed_layout_since_click)
2907 break;
2908 #endif
2909 // fall through
2910 case Event::Type::MouseMove:
2911 case Event::Type::MouseWheel:
2912 {
2913 #ifdef USE_TILE_LOCAL
2914 if (!hover_path.empty())
2915 {
2916 event.set_target(hover_path.back()->get_shared());
2917 for (auto w = hover_path.back(); w; w = w->_get_parent())
2918 if (w->on_event(event))
2919 return true;
2920 }
2921 #endif
2922 return false;
2923 }
2924
2925 case Event::Type::KeyDown:
2926 case Event::Type::KeyUp:
2927 {
2928 const auto top = top_child();
2929 if (!top)
2930 return false;
2931
2932 const auto key = static_cast<const KeyEvent&>(event).key();
2933 event.set_target(get_focused_widget()->get_shared());
2934
2935 // give hotkey handlers a chance to intercept this key; they are only
2936 // called if on a widget within the layout.
2937 if (event.type() == Event::Type::KeyDown)
2938 {
2939 // TODO: only emit if widget is visible
2940 bool hotkey_handled = Widget::slots.hotkey.emit_if([&](Widget* w){
2941 return top->is_ancestor_of(w->get_shared());
2942 }, event);
2943 if (hotkey_handled)
2944 return true;
2945 // TODO: eat the corresponding KeyUp event
2946 }
2947
2948 if (auto w = get_focused_widget())
2949 {
2950 if (w->on_event(event))
2951 return true;
2952
2953 if (w != state.default_focus && event.type() == Event::Type::KeyDown)
2954 {
2955 if (key_is_escape(key))
2956 {
2957 set_focused_widget(nullptr);
2958 return true;
2959 }
2960 if (key == '\t')
2961 {
2962 focus_next();
2963 return true;
2964 }
2965 if (key == CK_SHIFT_TAB)
2966 {
2967 focus_prev();
2968 return true;
2969 }
2970 }
2971
2972 for (w = w->_get_parent(); w; w = w->_get_parent())
2973 if (w->on_event(event))
2974 return true;
2975 }
2976
2977 if (event.type() == Event::Type::KeyDown)
2978 {
2979 if (key == '\t')
2980 {
2981 focus_next();
2982 return true;
2983 }
2984 if (key == CK_SHIFT_TAB)
2985 {
2986 focus_prev();
2987 return true;
2988 }
2989 }
2990 }
2991
2992 case Event::Type::FocusIn:
2993 case Event::Type::FocusOut:
2994 return event.target()->on_event(event);
2995
2996 default:
2997 for (auto w = event.target().get(); w; w = w->_get_parent())
2998 if (w->on_event(event))
2999 return true;
3000 return false;
3001 }
3002 return false;
3003 }
3004
3005 #ifdef DEBUG
debug_on_event(const wm_event & event)3006 bool UIRoot::debug_on_event(const wm_event& event)
3007 {
3008 if (event.type == WME_KEYDOWN && event.key.keysym.sym == CK_INSERT)
3009 {
3010 ui_root.debug_draw = !ui_root.debug_draw;
3011 ui_root.queue_layout();
3012 ui_root.expose_region({0, 0, INT_MAX, INT_MAX});
3013 return true;
3014 }
3015
3016 switch (event.type)
3017 {
3018 case WME_MOUSEBUTTONDOWN:
3019 case WME_MOUSEBUTTONUP:
3020 case WME_MOUSEMOTION:
3021 if (ui_root.debug_draw)
3022 {
3023 ui_root.queue_layout();
3024 ui_root.expose_region({0, 0, INT_MAX, INT_MAX});
3025 }
3026 default:
3027 break;
3028 }
3029
3030 return false;
3031 }
3032 #endif
3033
3034 #ifdef USE_TILE_WEB
update_synced_widgets()3035 void UIRoot::update_synced_widgets()
3036 {
3037 synced_widgets.clear();
3038
3039 function<void(Widget*)> recurse = [&](Widget* widget) {
3040 const auto id = widget->sync_id();
3041 if (!id.empty())
3042 {
3043 ASSERT(synced_widgets.count(id) == 0);
3044 synced_widgets[id] = widget;
3045 }
3046 widget->for_each_child_including_internal([&](shared_ptr<Widget>& ch) {
3047 recurse(ch.get());
3048 });
3049 };
3050
3051 if (const auto top = top_child())
3052 recurse(top.get());
3053 }
3054
sync_state()3055 void UIRoot::sync_state()
3056 {
3057 for (const auto& it : synced_widgets)
3058 it.second->sync_state_changed();
3059 }
3060
recv_ui_state_change(const JsonNode * json)3061 void UIRoot::recv_ui_state_change(const JsonNode *json)
3062 {
3063 const auto generation_id = json_find_member(json, "generation_id");
3064 if (!generation_id || generation_id->tag != JSON_NUMBER
3065 || generation_id->number_ != state.generation_id)
3066 {
3067 return;
3068 }
3069
3070 const auto has_focus = json_find_member(json, "has_focus");
3071 if (has_focus && (has_focus->tag != JSON_BOOL || !has_focus->bool_))
3072 return;
3073
3074 const auto widget_id = json_find_member(json, "widget_id");
3075 if (!widget_id)
3076 return;
3077 if (!(widget_id->tag == JSON_STRING || widget_id->tag == JSON_NULL))
3078 return;
3079 const auto widget = widget_id->tag == JSON_STRING ?
3080 synced_widgets.at(widget_id->string_) : nullptr;
3081
3082 unwind_bool recv(receiving_ui_state, true);
3083 if (has_focus)
3084 ui::set_focused_widget(widget);
3085 else if (widget)
3086 widget->sync_load_state(json);
3087 }
3088 #endif
3089
3090 #ifndef USE_TILE_LOCAL
clear_text_region(Region region,COLOURS bg)3091 static void clear_text_region(Region region, COLOURS bg)
3092 {
3093 region = region.aabb_intersect(scissor_stack.top());
3094 if (region.width <= 0 || region.height <= 0)
3095 return;
3096 textcolour(LIGHTGREY);
3097 textbackground(bg);
3098 for (int y=region.y; y < region.y+region.height; y++)
3099 {
3100 cgotoxy(region.x+1, y+1);
3101 cprintf("%*s", region.width, "");
3102 }
3103 }
3104 #endif
3105
3106 #ifdef USE_TILE
push_cutoff()3107 void push_cutoff()
3108 {
3109 int cutoff = static_cast<int>(ui_root.num_children());
3110 ui_root.cutoff_stack.push_back(cutoff);
3111 #ifdef USE_TILE_WEB
3112 tiles.push_ui_cutoff();
3113 #endif
3114 }
3115
pop_cutoff()3116 void pop_cutoff()
3117 {
3118 ui_root.cutoff_stack.pop_back();
3119 #ifdef USE_TILE_WEB
3120 tiles.pop_ui_cutoff();
3121 #endif
3122 }
3123 #endif
3124
push_layout(shared_ptr<Widget> root,KeymapContext km)3125 void push_layout(shared_ptr<Widget> root, KeymapContext km)
3126 {
3127 ui_root.push_child(move(root), km);
3128 #ifdef USE_TILE_WEB
3129 ui_root.sync_state();
3130 #endif
3131 }
3132
pop_layout()3133 void pop_layout()
3134 {
3135 ui_root.pop_child();
3136 #ifdef USE_TILE_LOCAL
3137 ui_root.update_hover_path();
3138 #else
3139 if (!has_layout())
3140 redraw_screen(false);
3141 #endif
3142 }
3143
top_layout()3144 shared_ptr<Widget> top_layout()
3145 {
3146 return ui_root.top_child();
3147 }
3148
resize(int w,int h)3149 void resize(int w, int h)
3150 {
3151 ui_root.resize(w, h);
3152 }
3153
remap_key(wm_event & event)3154 static void remap_key(wm_event &event)
3155 {
3156 keyseq keys = {event.key.keysym.sym};
3157 macro_buf_add_with_keymap(keys, ui_root.state.keymap);
3158 event.key.keysym.sym = macro_buf_get();
3159 ASSERT(event.key.keysym.sym != -1);
3160 }
3161
force_render()3162 void force_render()
3163 {
3164 ui_root.layout();
3165 ui_root.needs_paint = true;
3166 ui_root.render();
3167 ui_root.swap_buffers();
3168 }
3169
render()3170 void render()
3171 {
3172 ui_root.layout();
3173 ui_root.render();
3174 ui_root.swap_buffers();
3175 }
3176
pump_events(int wait_event_timeout)3177 void pump_events(int wait_event_timeout)
3178 {
3179 int macro_key = macro_buf_get();
3180
3181 #ifdef USE_TILE_LOCAL
3182 // Don't render while there are unhandled mousewheel events,
3183 // since these can come in faster than crawl can redraw.
3184 // unlike mousemotion events, we don't drop all but the last event
3185 // ...but if there are macro keys, we do need to layout (for menu UI)
3186 if (!wm->next_event_is(WME_MOUSEWHEEL) || macro_key != -1)
3187 #endif
3188 {
3189 ui_root.layout();
3190 #ifdef USE_TILE_WEB
3191 // On webtiles, we can't skip rendering while there are macro keys: a
3192 // crt screen may be opened and without a render() call, its text won't
3193 // won't be sent to the client(s). E.g: macro => iai
3194 ui_root.render();
3195 if (macro_key == -1)
3196 ui_root.swap_buffers();
3197 #else
3198 if (macro_key == -1)
3199 {
3200 ui_root.render();
3201 ui_root.swap_buffers();
3202 }
3203 #endif
3204 }
3205
3206 #ifdef USE_TILE_LOCAL
3207 // These WME_* events are also handled, at different times, by a
3208 // similar bit of code in tilesdl.cc. Roughly, that handling is used
3209 // during the main game display, and the this loop is used in the
3210 // main menu and when there are ui elements on top.
3211 // TODO: consolidate as much as possible
3212 wm_event event = {0};
3213 while (true)
3214 {
3215 if (macro_key != -1)
3216 {
3217 event.type = WME_KEYDOWN;
3218 event.key.keysym.sym = macro_key;
3219 break;
3220 }
3221
3222 if (!wm->wait_event(&event, wait_event_timeout))
3223 {
3224 if (wait_event_timeout == INT_MAX)
3225 continue;
3226 else
3227 return;
3228 }
3229
3230 // For consecutive mouse events, ignore all but the last,
3231 // since these can come in faster than crawl can redraw.
3232 if (event.type == WME_MOUSEMOTION && wm->next_event_is(WME_MOUSEMOTION))
3233 continue;
3234 if (event.type == WME_KEYDOWN && event.key.keysym.sym == 0)
3235 continue;
3236
3237 // translate any key events with the current keymap
3238 if (event.type == WME_KEYDOWN)
3239 remap_key(event);
3240 break;
3241 }
3242
3243 switch (event.type)
3244 {
3245 case WME_ACTIVEEVENT:
3246 // When game gains focus back then set mod state clean
3247 // to get rid of stupid Windows/SDL bug with Alt-Tab.
3248 if (event.active.gain != 0)
3249 {
3250 wm->set_mod_state(TILES_MOD_NONE);
3251 ui_root.needs_paint = true;
3252 }
3253 break;
3254
3255 case WME_QUIT:
3256 crawl_state.seen_hups++;
3257 break;
3258
3259 case WME_RESIZE:
3260 {
3261 // triggers ui::resize:
3262 tiles.resize_event(event.resize.w, event.resize.h);
3263 break;
3264 }
3265
3266 case WME_MOVE:
3267 if (tiles.update_dpi())
3268 ui_root.resize(wm->screen_width(), wm->screen_height());
3269 ui_root.needs_paint = true;
3270 break;
3271
3272 case WME_EXPOSE:
3273 ui_root.needs_paint = true;
3274 break;
3275
3276 case WME_MOUSEMOTION:
3277 // FIXME: move update_hover_path() into event delivery
3278 ui_root.update_hover_path();
3279 ui_root.on_event(event);
3280
3281 default:
3282 if (!ui_root.on_event(event) && event.type == WME_MOUSEBUTTONDOWN)
3283 {
3284 // If a mouse event wasn't handled, send it through again as a
3285 // fake key event, for compatibility
3286 int key;
3287 if (event.mouse_event.button == wm_mouse_event::LEFT)
3288 key = CK_MOUSE_CLICK;
3289 else if (event.mouse_event.button == wm_mouse_event::RIGHT)
3290 key = CK_MOUSE_CMD;
3291 else break;
3292
3293 wm_event ev = {0};
3294 ev.type = WME_KEYDOWN;
3295 ev.key.keysym.sym = key;
3296 ui_root.on_event(ev);
3297 }
3298 break;
3299 }
3300 #else
3301 if (wait_event_timeout <= 0) // resizing probably breaks this case
3302 return;
3303 set_getch_returns_resizes(true);
3304 int k = macro_key != -1 ? macro_key : getch_ck();
3305 set_getch_returns_resizes(false);
3306
3307 if (k == CK_RESIZE)
3308 {
3309 // This may be superfluous, since the resize handler may have already
3310 // resized the screen
3311 clrscr();
3312 console_shutdown();
3313 console_startup();
3314 ui_root.resize(get_number_of_cols(), get_number_of_lines());
3315 }
3316 else
3317 {
3318 wm_event ev = {0};
3319 ev.type = WME_KEYDOWN;
3320 ev.key.keysym.sym = k;
3321 if (macro_key == -1)
3322 remap_key(ev);
3323 ui_root.on_event(ev);
3324 }
3325 #endif
3326 }
3327
run_layout(shared_ptr<Widget> root,const bool & done,shared_ptr<Widget> initial_focus)3328 void run_layout(shared_ptr<Widget> root, const bool& done,
3329 shared_ptr<Widget> initial_focus)
3330 {
3331 push_layout(root);
3332 set_focused_widget(initial_focus.get());
3333 while (!done && !crawl_state.seen_hups)
3334 pump_events();
3335 pop_layout();
3336 }
3337
has_layout()3338 bool has_layout()
3339 {
3340 return ui_root.num_children() > 0;
3341 }
3342
restart_layout()3343 NORETURN void restart_layout()
3344 {
3345 throw UIRoot::RestartAllocation();
3346 }
3347
getch(KeymapContext km)3348 int getch(KeymapContext km)
3349 {
3350 // getch() can be called when there are no widget layouts, i.e.
3351 // older layout/rendering code is being used. these parts of code don't
3352 // set a dirty region, so we should do that now. One example of this
3353 // is mprf() called from yesno()
3354 ui_root.needs_paint = true;
3355
3356 int key;
3357 bool done = false;
3358 event_filter = [&](wm_event event) {
3359 // swallow all events except key presses here; we don't want any other
3360 // parts of the UI to react to anything while we're blocking for a key
3361 // press. An example: clicking shopping menu items when asked whether
3362 // to purchase already-selected items should not do anything.
3363 if (event.type != WME_KEYDOWN)
3364 return true;
3365 key = event.key.keysym.sym;
3366 done = true;
3367 return true;
3368 };
3369 unwind_var<KeymapContext> temp_keymap(ui_root.state.keymap, km);
3370 while (!done && !crawl_state.seen_hups)
3371 pump_events();
3372 event_filter = nullptr;
3373 return key;
3374 }
3375
delay(unsigned int ms)3376 void delay(unsigned int ms)
3377 {
3378 if (crawl_state.disables[DIS_DELAY])
3379 ms = 0;
3380
3381 auto start = std::chrono::high_resolution_clock::now();
3382 #ifdef USE_TILE_LOCAL
3383 int wait_event_timeout = ms;
3384 do
3385 {
3386 ui_root.expose_region({0, 0, INT_MAX, INT_MAX});
3387 pump_events(wait_event_timeout);
3388 auto now = std::chrono::high_resolution_clock::now();
3389 wait_event_timeout =
3390 std::chrono::duration_cast<std::chrono::milliseconds>(now - start)
3391 .count();
3392 }
3393 while ((unsigned)wait_event_timeout < ms && !crawl_state.seen_hups);
3394 #else
3395 constexpr int poll_interval = 10;
3396 while (!crawl_state.seen_hups)
3397 {
3398 auto now = std::chrono::high_resolution_clock::now();
3399 const int remaining = ms -
3400 std::chrono::duration_cast<std::chrono::milliseconds>(now - start)
3401 .count();
3402 if (remaining < 0)
3403 break;
3404 usleep(max(0, min(poll_interval, remaining)));
3405 if (kbhit())
3406 pump_events();
3407 }
3408 #endif
3409 }
3410
3411 /**
3412 * Is it possible to use UI calls, e.g. push_layout? The answer can be different
3413 * on different build targets; it is earlier on console than on local tiles.
3414 */
is_available()3415 bool is_available()
3416 {
3417 #ifdef USE_TILE_LOCAL
3418 // basically whether TilesFramework::initialise() has been called. This
3419 // isn't precisely right, so (TODO) some more work needs to be
3420 // done to figure out exactly what is needed to use the UI api minimally
3421 // without crashing.
3422 return wm && tiles.fonts_initialized();
3423 #else
3424 return crawl_state.io_inited;
3425 #endif
3426 }
3427
3428 /**
3429 * Show the terminal cursor at the given position on the next redraw.
3430 * The cursor is only shown if the cursor is enabled. 1-indexed.
3431 */
show_cursor_at(coord_def pos)3432 void show_cursor_at(coord_def pos)
3433 {
3434 ui_root.cursor_pos = pos;
3435 }
3436
show_cursor_at(int x,int y)3437 void show_cursor_at(int x, int y)
3438 {
3439 show_cursor_at(coord_def(x, y));
3440 }
3441
3442 /**
3443 * A basic progress bar popup. This is meant to be invoked in an RAII style;
3444 * the caller is responsible for regularly calling `advance_progress` in order
3445 * to actually trigger UI redraws.
3446 */
progress_popup(string title,int width)3447 progress_popup::progress_popup(string title, int width)
3448 : position(0), bar_width(width), no_more(crawl_state.show_more_prompt, false)
3449 {
3450 auto container = make_shared<Box>(Widget::VERT);
3451 container->set_cross_alignment(Widget::CENTER);
3452 #ifndef USE_TILE_LOCAL
3453 // Center the popup in console.
3454 // if webtiles browser ever uses this property, then this will probably
3455 // look bad there and need another solution. But right now, webtiles ignores
3456 // expand_h.
3457 container->expand_h = true;
3458 #endif
3459 formatted_string bar_string = get_progress_string(bar_width);
3460 progress_bar = make_shared<Text>(bar_string);
3461 auto title_text = make_shared<Text>(title);
3462 status_text = make_shared<Text>("");
3463 container->add_child(title_text);
3464 container->add_child(progress_bar);
3465 container->add_child(status_text);
3466 contents = make_shared<Popup>(container);
3467
3468 #ifdef USE_TILE_WEB
3469 tiles.json_open_object();
3470 tiles.json_write_string("title", title);
3471 tiles.json_write_string("bar_text", bar_string.to_colour_string());
3472 tiles.json_write_string("status", "");
3473 tiles.push_ui_layout("progress-bar", 1);
3474 tiles.flush_messages();
3475 contents->on_layout_pop([](){ tiles.pop_ui_layout(); });
3476 #endif
3477
3478 push_layout(move(contents));
3479 pump_events(0);
3480 }
3481
~progress_popup()3482 progress_popup::~progress_popup()
3483 {
3484 pop_layout();
3485
3486 #ifdef USE_TILE_WEB
3487 tiles.flush_messages();
3488 #endif
3489 }
3490
force_redraw()3491 void progress_popup::force_redraw()
3492 {
3493 #ifdef USE_TILE_WEB
3494 tiles.json_open_object();
3495 tiles.json_write_string("status", status_text->get_text());
3496 tiles.json_write_string("bar_text",
3497 progress_bar->get_text().to_colour_string());
3498 tiles.ui_state_change("progress-bar", 0);
3499 // need to manually flush messages in case the caller doesn't pause for
3500 // input.
3501 tiles.flush_messages();
3502 #endif
3503 pump_events(0);
3504 }
3505
set_status_text(string status)3506 void progress_popup::set_status_text(string status)
3507 {
3508 status_text->set_text(status);
3509 force_redraw();
3510 }
3511
advance_progress()3512 void progress_popup::advance_progress()
3513 {
3514 position++;
3515 formatted_string new_bar = get_progress_string(bar_width);
3516 progress_bar->set_text(new_bar);
3517 force_redraw();
3518 }
3519
get_progress_string(unsigned int len)3520 formatted_string progress_popup::get_progress_string(unsigned int len)
3521 {
3522 string bar = string(len, ' ');
3523 if (len < 3)
3524 return formatted_string(bar);
3525 const unsigned int center_pos = len + position % len;
3526 const bool up = center_pos % 2 == 0;
3527 const string marker = up ? "/o/" : "\\o\\";
3528 bar[(center_pos - 1) % len] = marker[0];
3529 bar[center_pos % len] = marker[1];
3530 bar[(center_pos + 1) % len] = marker[2];
3531 bar = string("<lightmagenta>") + bar + "</lightmagenta>";
3532 return formatted_string::parse_string(bar);
3533 }
3534
set_focused_widget(Widget * w)3535 void set_focused_widget(Widget* w)
3536 {
3537 static bool sent_focusout;
3538 static Widget* new_focus;
3539
3540 const auto top = top_layout();
3541
3542 if (!top)
3543 return;
3544
3545 if (w && !top->is_ancestor_of(w->get_shared()))
3546 return;
3547
3548 if (!w)
3549 w = ui_root.state.default_focus;
3550
3551 auto current_focus = ui_root.state.current_focus;
3552
3553 if (w == current_focus)
3554 return;
3555
3556 #ifdef USE_TILE_WEB
3557 tiles.json_open_object();
3558 tiles.json_write_string("msg", "ui-state-sync");
3559 tiles.json_write_bool("has_focus", true);
3560 tiles.json_write_string("widget_id", w->sync_id()); // "" means default
3561 tiles.json_write_bool("from_webtiles", ui_root.receiving_ui_state);
3562 tiles.json_write_int("generation_id", ui_root.state.generation_id);
3563 tiles.json_close_object();
3564 tiles.finish_message();
3565 #endif
3566
3567 new_focus = w;
3568
3569 if (current_focus && !sent_focusout)
3570 {
3571 sent_focusout = true;
3572 auto ev = FocusEvent(Event::Type::FocusOut);
3573 ev.set_target(current_focus->get_shared());
3574 ui_root.deliver_event(ev);
3575 }
3576
3577 if (new_focus != w)
3578 return;
3579
3580 ui_root.state.current_focus = new_focus;
3581
3582 sent_focusout = false;
3583
3584 if (new_focus)
3585 {
3586 auto ev = FocusEvent(Event::Type::FocusIn);
3587 ev.set_target(new_focus->get_shared());
3588 ui_root.deliver_event(ev);
3589 }
3590 }
3591
get_focused_widget()3592 Widget* get_focused_widget()
3593 {
3594 return ui_root.state.current_focus;
3595 }
3596
3597 #ifdef USE_TILE_WEB
recv_ui_state_change(const JsonNode * state)3598 void recv_ui_state_change(const JsonNode *state)
3599 {
3600 ui_root.recv_ui_state_change(state);
3601 }
3602
sync_ui_state()3603 void sync_ui_state()
3604 {
3605 ui_root.sync_state();
3606 }
3607
layout_generation_id()3608 int layout_generation_id()
3609 {
3610 return ui_root.next_generation_id;
3611 }
3612 #endif
3613
raise_event(Event & event)3614 bool raise_event(Event& event)
3615 {
3616 return ui_root.deliver_event(event);
3617 }
3618
3619 #ifdef USE_TILE_LOCAL
to_wm_event(const MouseEvent & ev)3620 wm_mouse_event to_wm_event(const MouseEvent &ev)
3621 {
3622 wm_mouse_event mev;
3623 mev.event = ev.type() == Event::Type::MouseMove ? wm_mouse_event::MOVE :
3624 ev.type() == Event::Type::MouseDown ? wm_mouse_event::PRESS :
3625 wm_mouse_event::WHEEL;
3626 mev.button = static_cast<wm_mouse_event::mouse_event_button>(ev.button());
3627 mev.mod = wm->get_mod_state();
3628 int x, y;
3629 mev.held = wm->get_mouse_state(&x, &y);
3630 mev.px = x;
3631 mev.py = y;
3632 return mev;
3633 }
3634 #endif
3635
3636 }
3637