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