Skip to main content

microui_redux/
container.rs

1//
2// Copyright 2022-Present (c) Raja Lehtihet & Wael El Oraiby
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are met:
6//
7// 1. Redistributions of source code must retain the above copyright notice,
8// this list of conditions and the following disclaimer.
9//
10// 2. Redistributions in binary form must reproduce the above copyright notice,
11// this list of conditions and the following disclaimer in the documentation
12// and/or other materials provided with the distribution.
13//
14// 3. Neither the name of the copyright holder nor the names of its contributors
15// may be used to endorse or promote products derived from this software without
16// specific prior written permission.
17//
18// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28// POSSIBILITY OF SUCH DAMAGE.
29//
30// -----------------------------------------------------------------------------
31// Ported to rust from https://github.com/rxi/microui/ and the original license
32//
33// Copyright (c) 2020 rxi
34//
35// Permission is hereby granted, free of charge, to any person obtaining a copy
36// of this software and associated documentation files (the "Software"), to
37// deal in the Software without restriction, including without limitation the
38// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
39// sell copies of the Software, and to permit persons to whom the Software is
40// furnished to do so, subject to the following conditions:
41//
42// The above copyright notice and this permission notice shall be included in
43// all copies or substantial portions of the Software.
44//
45// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
46// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
47// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
48// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
49// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
50// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
51// IN THE SOFTWARE.
52//
53use super::*;
54use crate::draw_context::DrawCtx;
55use crate::scrollbar::{scrollbar_base, scrollbar_drag_delta, scrollbar_max_scroll, scrollbar_thumb, ScrollAxis};
56use crate::text_layout::build_text_lines;
57use std::cell::RefCell;
58
59macro_rules! widget_layout {
60    ($(#[$meta:meta])* $name:ident, $state:ty, $builder:expr) => {
61        $(#[$meta])*
62        pub fn $name(&mut self, state: &mut $state) -> ResourceState {
63            let rect = self.next_widget_rect(state);
64            ($builder)(self, state, rect)
65        }
66    };
67    ($(#[$meta:meta])* $name:ident, $state:ty) => {
68        $(#[$meta])*
69        pub fn $name(&mut self, state: &mut $state) -> ResourceState {
70            self.handle_widget(state, None)
71        }
72    };
73}
74
75/// Arguments forwarded to custom rendering callbacks.
76pub struct CustomRenderArgs {
77    /// Rectangle describing the widget's content area.
78    pub content_area: Rect<i32>,
79    /// Final clipped region that is visible.
80    pub view: Rect<i32>, // clipped area
81    /// Latest mouse interaction affecting the widget.
82    pub mouse_event: MouseEvent,
83    /// Scroll delta consumed for this widget, if any.
84    pub scroll_delta: Option<Vec2i>,
85    /// Options provided when the widget was created.
86    pub widget_opt: WidgetOption,
87    /// Behaviour options provided when the widget was created.
88    pub behaviour_opt: WidgetBehaviourOption,
89    /// Currently active modifier keys.
90    pub key_mods: KeyMode,
91    /// Currently active navigation keys.
92    pub key_codes: KeyCode,
93    /// Text input collected while the widget was focused.
94    pub text_input: String,
95}
96
97/// Controls how text should wrap when rendered inside a container.
98#[derive(Copy, Clone, Debug, PartialEq, Eq)]
99pub enum TextWrap {
100    /// Render text on a single line without wrapping.
101    None,
102    /// Wrap text at word boundaries when it exceeds the cell width.
103    Word,
104}
105
106/// Draw commands recorded during container traversal.
107pub(crate) enum Command {
108    /// Pushes or pops a clip rectangle.
109    Clip {
110        /// Rect to clip against.
111        rect: Recti,
112    },
113    /// Draws a solid rectangle.
114    Recti {
115        /// Target rectangle.
116        rect: Recti,
117        /// Fill color.
118        color: Color,
119    },
120    /// Draws text.
121    Text {
122        /// Font to use.
123        font: FontId,
124        /// Top-left text position.
125        pos: Vec2i,
126        /// Text color.
127        color: Color,
128        /// UTF-8 string to render.
129        text: String,
130    },
131    /// Draws an icon from the atlas.
132    Icon {
133        /// Target rectangle.
134        rect: Recti,
135        /// Icon identifier.
136        id: IconId,
137        /// Tint color.
138        color: Color,
139    },
140    /// Draws an arbitrary image (slot or texture).
141    Image {
142        /// Target rectangle.
143        rect: Recti,
144        /// Image identifier.
145        image: Image,
146        /// Tint color.
147        color: Color,
148    },
149    /// Re-renders a slot before drawing it.
150    SlotRedraw {
151        /// Target rectangle.
152        rect: Recti,
153        /// Slot to update.
154        id: SlotId,
155        /// Tint color.
156        color: Color,
157        /// Callback generating pixels.
158        payload: Rc<dyn Fn(usize, usize) -> Color4b>,
159    },
160    /// Invokes a user callback for custom rendering.
161    CustomRender(CustomRenderArgs, Box<dyn FnMut(Dimensioni, &CustomRenderArgs)>),
162    /// Sentinel used when no command is enqueued.
163    None,
164}
165
166impl Default for Command {
167    fn default() -> Self {
168        Command::None
169    }
170}
171
172/// Core UI building block that records commands and hosts layouts.
173pub struct Container {
174    pub(crate) atlas: AtlasHandle,
175    /// Style used when drawing widgets in the container.
176    pub(crate) style: Rc<Style>,
177    /// Human-readable name for the container.
178    pub(crate) name: String,
179    /// Outer rectangle including frame and title.
180    pub(crate) rect: Recti,
181    /// Inner rectangle excluding frame/title.
182    pub(crate) body: Recti,
183    /// Size of the content region based on layout traversal.
184    pub(crate) content_size: Vec2i,
185    /// Accumulated scroll offset.
186    pub(crate) scroll: Vec2i,
187    /// Z-index used to order overlapping windows.
188    pub(crate) zindex: i32,
189    /// Recorded draw commands for this frame.
190    pub(crate) command_list: Vec<Command>,
191    /// Stack of clip rectangles applied while drawing.
192    pub(crate) clip_stack: Vec<Recti>,
193    pub(crate) layout: LayoutManager,
194    /// ID of the widget currently hovered, if any.
195    pub(crate) hover: Option<WidgetId>,
196    /// ID of the widget currently focused, if any.
197    pub(crate) focus: Option<WidgetId>,
198    /// Tracks whether focus changed this frame.
199    pub(crate) updated_focus: bool,
200    /// Internal state for the vertical scrollbar.
201    pub(crate) scrollbar_y_state: Internal,
202    /// Internal state for the horizontal scrollbar.
203    pub(crate) scrollbar_x_state: Internal,
204    /// Shared access to the input state.
205    pub(crate) input: Rc<RefCell<Input>>,
206    /// Cached per-frame input snapshot for widgets that need it.
207    input_snapshot: Option<Rc<InputSnapshot>>,
208    /// Whether this container is the current hover root.
209    pub(crate) in_hover_root: bool,
210    /// Tracks whether a popup was just opened this frame to avoid instant auto-close.
211    pub(crate) popup_just_opened: bool,
212    pending_scroll: Option<Vec2i>,
213    /// Determines whether container scrollbars and scroll consumption are enabled.
214    scroll_enabled: bool,
215
216    panels: Vec<ContainerHandle>,
217}
218
219impl Container {
220    pub(crate) fn new(name: &str, atlas: AtlasHandle, style: Rc<Style>, input: Rc<RefCell<Input>>) -> Self {
221        Self {
222            name: name.to_string(),
223            style,
224            atlas: atlas,
225            rect: Recti::default(),
226            body: Recti::default(),
227            content_size: Vec2i::default(),
228            scroll: Vec2i::default(),
229            zindex: 0,
230            command_list: Vec::default(),
231            clip_stack: Vec::default(),
232            hover: None,
233            focus: None,
234            updated_focus: false,
235            layout: LayoutManager::default(),
236            scrollbar_y_state: Internal::new("!scrollbary"),
237            scrollbar_x_state: Internal::new("!scrollbarx"),
238            popup_just_opened: false,
239            in_hover_root: false,
240            input: input,
241            input_snapshot: None,
242            pending_scroll: None,
243            scroll_enabled: true,
244
245            panels: Default::default(),
246        }
247    }
248
249    pub(crate) fn reset(&mut self) {
250        self.hover = None;
251        self.focus = None;
252        self.updated_focus = false;
253        self.in_hover_root = false;
254        self.input_snapshot = None;
255        self.pending_scroll = None;
256        self.scroll_enabled = true;
257    }
258
259    pub(crate) fn prepare(&mut self) {
260        self.command_list.clear();
261        assert!(self.clip_stack.len() == 0);
262        self.panels.clear();
263        self.input_snapshot = None;
264        self.pending_scroll = None;
265        self.scroll_enabled = true;
266    }
267
268    pub(crate) fn seed_pending_scroll(&mut self, delta: Option<Vec2i>) {
269        self.pending_scroll = delta;
270    }
271
272    #[inline(never)]
273    pub(crate) fn render<R: Renderer>(&mut self, canvas: &mut Canvas<R>) {
274        for command in self.command_list.drain(0..) {
275            match command {
276                Command::Text { text, pos, color, font } => {
277                    canvas.draw_chars(font, &text, pos, color);
278                }
279                Command::Recti { rect, color } => {
280                    canvas.draw_rect(rect, color);
281                }
282                Command::Icon { id, rect, color } => {
283                    canvas.draw_icon(id, rect, color);
284                }
285                Command::Clip { rect } => {
286                    canvas.set_clip_rect(rect);
287                }
288                Command::Image { rect, image, color } => {
289                    canvas.draw_image(image, rect, color);
290                }
291                Command::SlotRedraw { rect, id, color, payload } => {
292                    canvas.draw_slot_with_function(id, rect, color, payload.clone());
293                }
294                Command::CustomRender(mut cra, mut f) => {
295                    canvas.flush();
296                    let prev_clip = canvas.current_clip_rect();
297                    let merged_clip = match prev_clip.intersect(&cra.view) {
298                        Some(rect) => rect,
299                        None => Recti::new(cra.content_area.x, cra.content_area.y, 0, 0),
300                    };
301                    canvas.set_clip_rect(merged_clip);
302                    cra.view = merged_clip;
303                    (*f)(canvas.current_dimension(), &cra);
304                    canvas.flush();
305                    canvas.set_clip_rect(prev_clip);
306                }
307                Command::None => (),
308            }
309        }
310
311        for ap in &mut self.panels {
312            ap.render(canvas)
313        }
314    }
315
316    fn draw_ctx(&mut self) -> DrawCtx<'_> {
317        DrawCtx::new(&mut self.command_list, &mut self.clip_stack, self.style.as_ref(), &self.atlas)
318    }
319
320    /// Pushes a new clip rectangle combined with the previous clip.
321    pub fn push_clip_rect(&mut self, rect: Recti) {
322        let mut draw = self.draw_ctx();
323        draw.push_clip_rect(rect);
324    }
325
326    /// Restores the previous clip rectangle from the stack.
327    pub fn pop_clip_rect(&mut self) {
328        let mut draw = self.draw_ctx();
329        draw.pop_clip_rect();
330    }
331
332    /// Returns the active clip rectangle, or an unclipped rect when the stack is empty.
333    pub fn get_clip_rect(&mut self) -> Recti {
334        self.draw_ctx().current_clip_rect()
335    }
336
337    /// Determines whether `r` is fully visible, partially visible, or completely clipped.
338    pub fn check_clip(&mut self, r: Recti) -> Clip {
339        self.draw_ctx().check_clip(r)
340    }
341
342    /// Adjusts the current clip rectangle.
343    pub fn set_clip(&mut self, rect: Recti) {
344        let mut draw = self.draw_ctx();
345        draw.set_clip(rect);
346    }
347
348    /// Manually updates which widget owns focus.
349    pub fn set_focus(&mut self, widget_id: Option<WidgetId>) {
350        self.focus = widget_id;
351        self.updated_focus = true;
352    }
353
354    /// Records a filled rectangle draw command.
355    pub fn draw_rect(&mut self, rect: Recti, color: Color) {
356        let mut draw = self.draw_ctx();
357        draw.draw_rect(rect, color);
358    }
359
360    /// Records a rectangle outline.
361    pub fn draw_box(&mut self, r: Recti, color: Color) {
362        let mut draw = self.draw_ctx();
363        draw.draw_box(r, color);
364    }
365
366    /// Records a text draw command.
367    pub fn draw_text(&mut self, font: FontId, str: &str, pos: Vec2i, color: Color) {
368        let mut draw = self.draw_ctx();
369        draw.draw_text(font, str, pos, color);
370    }
371
372    /// Records an icon draw command.
373    pub fn draw_icon(&mut self, id: IconId, rect: Recti, color: Color) {
374        let mut draw = self.draw_ctx();
375        draw.draw_icon(id, rect, color);
376    }
377
378    /// Records a slot draw command.
379    pub fn draw_slot(&mut self, id: SlotId, rect: Recti, color: Color) {
380        let mut draw = self.draw_ctx();
381        draw.push_image(Image::Slot(id), rect, color);
382    }
383
384    /// Records a slot redraw that uses a callback to fill pixels.
385    pub fn draw_slot_with_function(&mut self, id: SlotId, rect: Recti, color: Color, f: Rc<dyn Fn(usize, usize) -> Color4b>) {
386        let mut draw = self.draw_ctx();
387        draw.draw_slot_with_function(id, rect, color, f);
388    }
389
390    #[inline(never)]
391    /// Draws multi-line text within the container without wrapping.
392    pub fn text(&mut self, text: &str) {
393        self.text_with_wrap(text, TextWrap::None);
394    }
395
396    #[inline(never)]
397    /// Draws multi-line text within the container using the provided wrapping mode.
398    /// The block is rendered inside an internal column with zero spacing so consecutive
399    /// lines sit back-to-back while the outer widget spacing/padding remains intact.
400    pub fn text_with_wrap(&mut self, text: &str, wrap: TextWrap) {
401        if text.is_empty() {
402            return;
403        }
404        let style = self.style.as_ref();
405        let font = style.font;
406        let color = style.colors[ControlColor::Text as usize];
407        let line_height = self.atlas.get_font_height(font) as i32;
408        let baseline = self.atlas.get_font_baseline(font);
409        let saved_spacing = self.layout.style.spacing;
410        self.layout.style.spacing = 0;
411        self.column(|ui| {
412            ui.layout.row(&[SizePolicy::Remainder(0)], SizePolicy::Fixed(line_height));
413            let first_rect = ui.layout.next();
414            let max_width = first_rect.width;
415            let mut lines = build_text_lines(text, wrap, max_width, font, &ui.atlas);
416            if text.ends_with('\n') {
417                if let Some(last) = lines.last() {
418                    if last.start == text.len() && last.end == text.len() {
419                        lines.pop();
420                    }
421                }
422            }
423            for (idx, line) in lines.iter().enumerate() {
424                let r = if idx == 0 { first_rect } else { ui.layout.next() };
425                let line_top = Self::baseline_aligned_top(r, line_height, baseline);
426                let slice = &text[line.start..line.end];
427                if !slice.is_empty() {
428                    ui.draw_text(font, slice, vec2(r.x, line_top), color);
429                }
430            }
431        });
432        self.layout.style.spacing = saved_spacing;
433    }
434
435    /// Draws a frame and optional border using the specified color.
436    pub fn draw_frame(&mut self, rect: Recti, colorid: ControlColor) {
437        let mut draw = self.draw_ctx();
438        draw.draw_frame(rect, colorid);
439    }
440
441    /// Draws a widget background, applying hover/focus accents when needed.
442    pub fn draw_widget_frame(&mut self, widget_id: WidgetId, rect: Recti, colorid: ControlColor, opt: WidgetOption) {
443        let focused = self.focus == Some(widget_id);
444        let hovered = self.hover == Some(widget_id);
445        let mut draw = self.draw_ctx();
446        draw.draw_widget_frame(focused, hovered, rect, colorid, opt);
447    }
448
449    /// Draws a container frame, skipping rendering when the option disables it.
450    pub fn draw_container_frame(&mut self, widget_id: WidgetId, rect: Recti, mut colorid: ControlColor, opt: ContainerOption) {
451        if opt.has_no_frame() {
452            return;
453        }
454
455        if self.focus == Some(widget_id) {
456            colorid.focus()
457        } else if self.hover == Some(widget_id) {
458            colorid.hover()
459        }
460        let mut draw = self.draw_ctx();
461        draw.draw_frame(rect, colorid);
462    }
463
464    #[inline(never)]
465    /// Draws widget text with the appropriate alignment flags.
466    pub fn draw_control_text(&mut self, str: &str, rect: Recti, colorid: ControlColor, opt: WidgetOption) {
467        let mut draw = self.draw_ctx();
468        draw.draw_control_text(str, rect, colorid, opt);
469    }
470
471    /// Returns `true` if the cursor is inside `rect` and the container owns the hover root.
472    pub fn mouse_over(&mut self, rect: Recti, in_hover_root: bool) -> bool {
473        let clip_rect = self.get_clip_rect();
474        rect.contains(&self.input.borrow().mouse_pos) && clip_rect.contains(&self.input.borrow().mouse_pos) && in_hover_root
475    }
476
477    fn update_control_with_opts(&mut self, widget_id: WidgetId, rect: Recti, opt: WidgetOption, bopt: WidgetBehaviourOption) -> ControlState {
478        let in_hover_root = self.in_hover_root;
479        let mouseover = self.mouse_over(rect, in_hover_root);
480        if self.focus == Some(widget_id) {
481            // is this the same ID of the focused widget? by default set it to true unless otherwise
482            self.updated_focus = true;
483        }
484        if opt.is_not_interactive() {
485            return ControlState::default();
486        }
487        if mouseover && self.input.borrow().mouse_down.is_none() {
488            self.hover = Some(widget_id);
489        }
490        if self.focus == Some(widget_id) {
491            if !self.input.borrow().mouse_pressed.is_none() && !mouseover {
492                self.set_focus(None);
493            }
494            if self.input.borrow().mouse_down.is_none() && !opt.is_holding_focus() {
495                self.set_focus(None);
496            }
497        }
498        if self.hover == Some(widget_id) {
499            if !self.input.borrow().mouse_pressed.is_none() {
500                self.set_focus(Some(widget_id));
501            } else if !mouseover {
502                self.hover = None;
503            }
504        }
505
506        let mut scroll = None;
507        if bopt.is_grab_scroll() && self.hover == Some(widget_id) {
508            if let Some(delta) = self.pending_scroll {
509                if delta.x != 0 || delta.y != 0 {
510                    self.pending_scroll = None;
511                    scroll = Some(delta);
512                }
513            }
514        }
515
516        if self.focus == Some(widget_id) {
517            let mouse_pos = self.input.borrow().mouse_pos;
518            let origin = vec2(self.body.x, self.body.y);
519            self.input.borrow_mut().rel_mouse_pos = mouse_pos - origin;
520        }
521
522        let input = self.input.borrow();
523        let focused = self.focus == Some(widget_id);
524        let hovered = self.hover == Some(widget_id);
525        let clicked = focused && input.mouse_pressed.is_left();
526        let active = focused && input.mouse_down.is_left();
527        drop(input);
528
529        ControlState {
530            hovered,
531            focused,
532            clicked,
533            active,
534            scroll_delta: scroll,
535        }
536    }
537
538    #[inline(never)]
539    /// Updates hover/focus state for the widget described by `widget_id` and optionally consumes scroll.
540    pub fn update_control<W: Widget>(&mut self, widget_id: WidgetId, rect: Recti, state: &W) -> ControlState {
541        self.update_control_with_opts(widget_id, rect, *state.widget_opt(), *state.behaviour_opt())
542    }
543
544    fn snapshot_input(&mut self) -> Rc<InputSnapshot> {
545        if let Some(snapshot) = &self.input_snapshot {
546            return snapshot.clone();
547        }
548
549        let input = self.input.borrow();
550        let snapshot = Rc::new(InputSnapshot {
551            mouse_pos: input.mouse_pos,
552            mouse_delta: input.mouse_delta,
553            mouse_down: input.mouse_down,
554            mouse_pressed: input.mouse_pressed,
555            key_mods: input.key_down,
556            key_pressed: input.key_pressed,
557            key_codes: input.key_code_down,
558            key_code_pressed: input.key_code_pressed,
559            text_input: input.input_text.clone(),
560        });
561        self.input_snapshot = Some(snapshot.clone());
562        snapshot
563    }
564
565    pub(crate) fn widget_ctx(&mut self, widget_id: WidgetId, rect: Recti, input: Option<Rc<InputSnapshot>>) -> WidgetCtx<'_> {
566        WidgetCtx::new(
567            widget_id,
568            rect,
569            &mut self.command_list,
570            &mut self.clip_stack,
571            self.style.as_ref(),
572            &self.atlas,
573            &mut self.focus,
574            &mut self.updated_focus,
575            self.in_hover_root,
576            input,
577        )
578    }
579
580    fn run_widget<W: Widget>(
581        &mut self,
582        state: &mut W,
583        rect: Recti,
584        input: Option<Rc<InputSnapshot>>,
585        opt: WidgetOption,
586        bopt: WidgetBehaviourOption,
587    ) -> (ControlState, ResourceState) {
588        let widget_id = widget_id_of(state);
589        let control = self.update_control_with_opts(widget_id, rect, opt, bopt);
590        let mut ctx = self.widget_ctx(widget_id, rect, input);
591        let res = state.handle(&mut ctx, &control);
592        (control, res)
593    }
594
595    fn next_widget_rect<W: Widget>(&mut self, state: &W) -> Recti {
596        // Widget helpers measure before placing so Auto rows can follow each widget's intrinsic size.
597        let body = self.layout.current_body();
598        let avail = Dimensioni::new(body.width.max(0), body.height.max(0));
599        let preferred = state.preferred_size(self.style.as_ref(), &self.atlas, avail);
600        self.layout.next_with_preferred(preferred)
601    }
602
603    fn handle_widget<W: Widget>(&mut self, state: &mut W, input: Option<Rc<InputSnapshot>>) -> ResourceState {
604        let rect = self.next_widget_rect(state);
605        let opt = *state.widget_opt();
606        let bopt = *state.behaviour_opt();
607        let (_, res) = self.run_widget(state, rect, input, opt, bopt);
608        res
609    }
610
611    fn handle_widget_in_rect<W: Widget>(
612        &mut self,
613        state: &mut W,
614        rect: Recti,
615        input: Option<Rc<InputSnapshot>>,
616        opt: WidgetOption,
617        bopt: WidgetBehaviourOption,
618    ) -> ResourceState {
619        let (_, res) = self.run_widget(state, rect, input, opt, bopt);
620        res
621    }
622
623    /// Resets transient per-frame state after widgets have been processed.
624    pub fn finish(&mut self) {
625        if !self.updated_focus {
626            self.focus = None;
627        }
628        self.updated_focus = false;
629    }
630
631    /// Returns the outer container rectangle.
632    pub fn rect(&self) -> Recti {
633        self.rect
634    }
635
636    /// Sets the outer container rectangle.
637    pub fn set_rect(&mut self, rect: Recti) {
638        self.rect = rect;
639    }
640
641    /// Returns the inner container body rectangle.
642    pub fn body(&self) -> Recti {
643        self.body
644    }
645
646    /// Returns the current scroll offset.
647    pub fn scroll(&self) -> Vec2i {
648        self.scroll
649    }
650
651    /// Sets the current scroll offset.
652    pub fn set_scroll(&mut self, scroll: Vec2i) {
653        self.scroll = scroll;
654    }
655
656    /// Returns the content size derived from layout traversal.
657    pub fn content_size(&self) -> Vec2i {
658        self.content_size
659    }
660
661    fn node_scope<F: FnOnce(&mut Self)>(&mut self, state: &mut Node, indent: bool, f: F) -> NodeStateValue {
662        self.layout.row(&[SizePolicy::Remainder(0)], SizePolicy::Auto);
663        let r = self.next_widget_rect(state);
664        let opt = *state.widget_opt();
665        let bopt = *state.behaviour_opt();
666        let _ = self.handle_widget_in_rect(state, r, None, opt, bopt);
667        if state.state.is_expanded() {
668            if indent {
669                let indent_size = self.style.as_ref().indent;
670                self.layout.adjust_indent(indent_size);
671                f(self);
672                self.layout.adjust_indent(-indent_size);
673            } else {
674                f(self);
675            }
676        }
677        state.state
678    }
679
680    /// Builds a collapsible header row that executes `f` when expanded.
681    pub fn header<F: FnOnce(&mut Self)>(&mut self, state: &mut Node, f: F) -> NodeStateValue {
682        self.node_scope(state, false, f)
683    }
684
685    /// Builds a tree node with automatic indentation while expanded.
686    pub fn treenode<F: FnOnce(&mut Self)>(&mut self, state: &mut Node, f: F) -> NodeStateValue {
687        self.node_scope(state, true, f)
688    }
689
690    fn clamp(x: i32, a: i32, b: i32) -> i32 {
691        min(b, max(a, x))
692    }
693
694    /// Returns the y coordinate where a line of text should start so its baseline sits at the control midpoint.
695    fn baseline_aligned_top(rect: Recti, line_height: i32, baseline: i32) -> i32 {
696        if rect.height >= line_height {
697            return rect.y + (rect.height - line_height) / 2;
698        }
699
700        let baseline_center = rect.y + rect.height / 2;
701        let min_top = rect.y + rect.height - line_height;
702        let max_top = rect.y;
703        Self::clamp(baseline_center - baseline, min_top, max_top)
704    }
705
706    fn vertical_text_padding(padding: i32) -> i32 {
707        max(1, padding / 2)
708    }
709
710    pub(crate) fn consume_pending_scroll(&mut self) {
711        if !self.scroll_enabled {
712            return;
713        }
714        let delta = match self.pending_scroll {
715            Some(delta) if delta.x != 0 || delta.y != 0 => delta,
716            _ => return,
717        };
718
719        let mut consumed = false;
720        let mut scroll = self.scroll;
721        let mut content_size = self.content_size;
722        let padding = self.style.as_ref().padding * 2;
723        content_size.x += padding;
724        content_size.y += padding;
725        let body = self.body;
726
727        let maxscroll_y = content_size.y - body.height;
728        if delta.y != 0 && maxscroll_y > 0 && body.height > 0 {
729            let new_scroll = Self::clamp(scroll.y + delta.y, 0, maxscroll_y);
730            if new_scroll != scroll.y {
731                scroll.y = new_scroll;
732                consumed = true;
733            }
734        }
735
736        let maxscroll_x = content_size.x - body.width;
737        if delta.x != 0 && maxscroll_x > 0 && body.width > 0 {
738            let new_scroll = Self::clamp(scroll.x + delta.x, 0, maxscroll_x);
739            if new_scroll != scroll.x {
740                scroll.x = new_scroll;
741                consumed = true;
742            }
743        }
744
745        if consumed {
746            self.scroll = scroll;
747            self.pending_scroll = None;
748        }
749    }
750
751    #[inline(never)]
752    fn scrollbars(&mut self, body: &mut Recti) {
753        let (scrollbar_size, padding, thumb_size) = {
754            let style = self.style.as_ref();
755            (style.scrollbar_size, style.padding, style.thumb_size)
756        };
757        let sz = scrollbar_size;
758        let mut cs: Vec2i = self.content_size;
759        cs.x += padding * 2;
760        cs.y += padding * 2;
761        let base_body = *body;
762        self.push_clip_rect(body.clone());
763        if cs.y > base_body.height {
764            body.width -= sz;
765        }
766        if cs.x > base_body.width {
767            body.height -= sz;
768        }
769        let body = *body;
770        let maxscroll = scrollbar_max_scroll(cs.y, body.height);
771        if maxscroll > 0 && body.height > 0 {
772            let scrollbar_y_id = widget_id_of(&self.scrollbar_y_state);
773            let base = scrollbar_base(ScrollAxis::Vertical, body, scrollbar_size);
774            let control = self.update_control_with_opts(scrollbar_y_id, base, self.scrollbar_y_state.opt, self.scrollbar_y_state.bopt);
775            {
776                let mut ctx = WidgetCtx::new(
777                    scrollbar_y_id,
778                    base,
779                    &mut self.command_list,
780                    &mut self.clip_stack,
781                    self.style.as_ref(),
782                    &self.atlas,
783                    &mut self.focus,
784                    &mut self.updated_focus,
785                    self.in_hover_root,
786                    None,
787                );
788                let _ = self.scrollbar_y_state.handle(&mut ctx, &control);
789            }
790            if control.active {
791                let delta = scrollbar_drag_delta(ScrollAxis::Vertical, self.input.borrow().mouse_delta, cs.y, base);
792                self.scroll.y += delta;
793            }
794            self.scroll.y = Self::clamp(self.scroll.y, 0, maxscroll);
795            self.draw_frame(base, ControlColor::ScrollBase);
796            let thumb = scrollbar_thumb(ScrollAxis::Vertical, base, body.height, cs.y, self.scroll.y, thumb_size);
797            self.draw_frame(thumb, ControlColor::ScrollThumb);
798        } else {
799            self.scroll.y = 0;
800        }
801        let maxscroll_0 = scrollbar_max_scroll(cs.x, body.width);
802        if maxscroll_0 > 0 && body.width > 0 {
803            let scrollbar_x_id = widget_id_of(&self.scrollbar_x_state);
804            let base_0 = scrollbar_base(ScrollAxis::Horizontal, body, scrollbar_size);
805            let control = self.update_control_with_opts(scrollbar_x_id, base_0, self.scrollbar_x_state.opt, self.scrollbar_x_state.bopt);
806            {
807                let mut ctx = WidgetCtx::new(
808                    scrollbar_x_id,
809                    base_0,
810                    &mut self.command_list,
811                    &mut self.clip_stack,
812                    self.style.as_ref(),
813                    &self.atlas,
814                    &mut self.focus,
815                    &mut self.updated_focus,
816                    self.in_hover_root,
817                    None,
818                );
819                let _ = self.scrollbar_x_state.handle(&mut ctx, &control);
820            }
821            if control.active {
822                let delta = scrollbar_drag_delta(ScrollAxis::Horizontal, self.input.borrow().mouse_delta, cs.x, base_0);
823                self.scroll.x += delta;
824            }
825            self.scroll.x = Self::clamp(self.scroll.x, 0, maxscroll_0);
826            self.draw_frame(base_0, ControlColor::ScrollBase);
827            let thumb_0 = scrollbar_thumb(ScrollAxis::Horizontal, base_0, body.width, cs.x, self.scroll.x, thumb_size);
828            self.draw_frame(thumb_0, ControlColor::ScrollThumb);
829        } else {
830            self.scroll.x = 0;
831        }
832        self.pop_clip_rect();
833    }
834
835    /// Configures layout state for the container's client area, handling scrollbars when necessary.
836    pub fn push_container_body(&mut self, body: Recti, _opt: ContainerOption, bopt: WidgetBehaviourOption) {
837        let mut body = body;
838        self.scroll_enabled = !bopt.is_no_scroll();
839        if self.scroll_enabled {
840            self.scrollbars(&mut body);
841        }
842        let (layout_padding, style_padding, font, style_clone) = {
843            let style = self.style.as_ref();
844            (-style.padding, style.padding, style.font, style.clone())
845        };
846        let scroll = self.scroll;
847        self.layout.reset(expand_rect(body, layout_padding), scroll);
848        self.layout.style = style_clone;
849        let font_height = self.atlas.get_font_height(font) as i32;
850        let vertical_pad = Self::vertical_text_padding(style_padding);
851        let icon_height = self.atlas.get_icon_size(EXPAND_DOWN_ICON).height;
852        let default_height = max(font_height + vertical_pad * 2, icon_height);
853        self.layout.set_default_cell_height(default_height);
854        self.body = body;
855    }
856
857    fn pop_panel(&mut self, panel: &mut ContainerHandle) {
858        let layout_body = panel.inner().layout.current_body();
859        let layout_max = panel.inner().layout.current_max();
860        let container = &mut panel.inner_mut();
861
862        match layout_max {
863            None => (),
864            Some(lm) => container.content_size = Vec2i::new(lm.x - layout_body.x, lm.y - layout_body.y),
865        }
866
867        container.layout.pop_scope();
868    }
869
870    #[inline(never)]
871    fn begin_panel(&mut self, panel: &mut ContainerHandle, opt: ContainerOption, bopt: WidgetBehaviourOption) {
872        let rect = self.layout.next();
873        let container = &mut panel.inner_mut();
874        container.prepare();
875        container.style = self.style.clone();
876
877        container.rect = rect;
878        if !opt.has_no_frame() {
879            self.draw_frame(rect, ControlColor::PanelBG);
880        }
881
882        container.in_hover_root = self.in_hover_root;
883        if self.pending_scroll.is_some() && self.mouse_over(rect, self.in_hover_root) {
884            container.pending_scroll = self.pending_scroll.take();
885        }
886        container.push_container_body(rect, opt, bopt);
887        let clip_rect = container.body;
888        container.push_clip_rect(clip_rect);
889    }
890
891    fn end_panel(&mut self, panel: &mut ContainerHandle) {
892        panel.inner_mut().pop_clip_rect();
893        self.pop_panel(panel);
894        {
895            let mut inner = panel.inner_mut();
896            inner.consume_pending_scroll();
897            let pending = inner.pending_scroll.take();
898            if self.pending_scroll.is_none() {
899                self.pending_scroll = pending;
900            }
901        }
902        self.panels.push(panel.clone())
903    }
904
905    /// Embeds another container handle inside the current layout.
906    pub fn panel<F: FnOnce(&mut ContainerHandle)>(&mut self, panel: &mut ContainerHandle, opt: ContainerOption, bopt: WidgetBehaviourOption, f: F) {
907        self.begin_panel(panel, opt, bopt);
908
909        // call the panel function
910        f(panel);
911
912        self.end_panel(panel);
913    }
914
915    /// Temporarily overrides the row definition and restores it after `f` executes.
916    pub fn with_row<F: FnOnce(&mut Self)>(&mut self, widths: &[SizePolicy], height: SizePolicy, f: F) {
917        let snapshot = self.layout.snapshot_flow_state();
918        self.layout.row(widths, height);
919        f(self);
920        self.layout.restore_flow_state(snapshot);
921    }
922
923    /// Temporarily uses a vertical stack flow and restores the previous flow after `f` executes.
924    ///
925    /// Each `next_cell`/widget call in the scope gets a dedicated row using `height`.
926    /// Width defaults to `Remainder(0)` so cells fill available horizontal space.
927    pub fn stack<F: FnOnce(&mut Self)>(&mut self, height: SizePolicy, f: F) {
928        self.stack_with_width_direction(SizePolicy::Remainder(0), height, StackDirection::TopToBottom, f);
929    }
930
931    /// Same as [`Container::stack`], but controls whether items are emitted top-down or bottom-up.
932    pub fn stack_direction<F: FnOnce(&mut Self)>(&mut self, height: SizePolicy, direction: StackDirection, f: F) {
933        self.stack_with_width_direction(SizePolicy::Remainder(0), height, direction, f);
934    }
935
936    /// Same as [`Container::stack`], but allows overriding the stack cell width policy.
937    pub fn stack_with_width<F: FnOnce(&mut Self)>(&mut self, width: SizePolicy, height: SizePolicy, f: F) {
938        self.stack_with_width_direction(width, height, StackDirection::TopToBottom, f);
939    }
940
941    /// Same as [`Container::stack_with_width`], but controls whether items are emitted top-down or bottom-up.
942    pub fn stack_with_width_direction<F: FnOnce(&mut Self)>(&mut self, width: SizePolicy, height: SizePolicy, direction: StackDirection, f: F) {
943        let snapshot = self.layout.snapshot_flow_state();
944        if direction == StackDirection::TopToBottom {
945            self.layout.stack(width, height);
946        } else {
947            self.layout.stack_with_direction(width, height, direction);
948        }
949        f(self);
950        self.layout.restore_flow_state(snapshot);
951    }
952
953    /// Creates a nested column scope where each call to `next_cell` yields a single column.
954    pub fn column<F: FnOnce(&mut Self)>(&mut self, f: F) {
955        self.layout.begin_column();
956        f(self);
957        self.layout.end_column();
958    }
959
960    /// Returns the next raw layout cell rectangle.
961    ///
962    /// Unlike widget helper methods (`button`, `textbox`, etc.), this does not consult
963    /// a widget's `preferred_size`; it uses only the current row/column policies.
964    pub fn next_cell(&mut self) -> Recti {
965        self.layout.next()
966    }
967
968    /// Replaces the container's style.
969    pub fn set_style(&mut self, style: Style) {
970        self.style = Rc::new(style);
971    }
972
973    /// Returns a copy of the current style.
974    pub fn get_style(&self) -> Style {
975        (*self.style).clone()
976    }
977
978    /// Displays static text using the default text color.
979    ///
980    /// This helper uses intrinsic text metrics to request a preferred cell size.
981    pub fn label(&mut self, text: &str) {
982        let (font, padding) = {
983            let style = self.style.as_ref();
984            (style.font, style.padding.max(0))
985        };
986        let text_width = if text.is_empty() {
987            0
988        } else {
989            self.atlas.get_text_size(font, text).width.max(0)
990        };
991        let line_height = self.atlas.get_font_height(font) as i32;
992        let vertical_pad = Self::vertical_text_padding(padding);
993        let preferred = Dimensioni::new(
994            text_width.saturating_add(padding.saturating_mul(2)),
995            line_height.saturating_add(vertical_pad.saturating_mul(2)),
996        );
997        let layout = self.layout.next_with_preferred(preferred);
998        self.draw_control_text(text, layout, ControlColor::Text, WidgetOption::NONE);
999    }
1000
1001    widget_layout!(
1002        #[inline(never)]
1003        /// Draws a button using the provided persistent state.
1004        button,
1005        Button
1006    );
1007
1008    widget_layout!(
1009        /// Renders a list entry that only highlights while hovered or active.
1010        list_item,
1011        ListItem
1012    );
1013
1014    widget_layout!(
1015        #[inline(never)]
1016        /// Shim for list boxes that only fills on hover or click.
1017        list_box,
1018        ListBox
1019    );
1020
1021    #[inline(never)]
1022    /// Draws the combo box header, clamps the selected index, and returns the popup anchor.
1023    /// The caller is responsible for opening the popup and updating `state.selected` from its list.
1024    pub fn combo_box<S: AsRef<str>>(&mut self, state: &mut Combo, items: &[S]) -> (Recti, bool, ResourceState) {
1025        state.update_items(items);
1026        let header = self.next_widget_rect(state);
1027        let opt = *state.widget_opt();
1028        let bopt = *state.behaviour_opt();
1029        let res = self.handle_widget_in_rect(state, header, None, opt, bopt);
1030        let header_clicked = res.is_submitted();
1031        let anchor = rect(header.x, header.y + header.height, header.width, 1);
1032        (anchor, header_clicked, res)
1033    }
1034
1035    widget_layout!(
1036        #[inline(never)]
1037        /// Draws a checkbox labeled with `label` and toggles `state` when clicked.
1038        checkbox,
1039        Checkbox
1040    );
1041
1042    #[inline(never)]
1043    fn input_to_mouse_event(&self, control: &ControlState, input: &InputSnapshot, rect: Recti) -> MouseEvent {
1044        let orig = Vec2i::new(rect.x, rect.y);
1045
1046        let prev_pos = input.mouse_pos - input.mouse_delta - orig;
1047        let curr_pos = input.mouse_pos - orig;
1048        let mouse_down = input.mouse_down;
1049        let mouse_pressed = input.mouse_pressed;
1050
1051        if control.focused && mouse_down.is_left() {
1052            return MouseEvent::Drag { prev_pos, curr_pos };
1053        }
1054
1055        if control.hovered && mouse_pressed.is_left() {
1056            return MouseEvent::Click(curr_pos);
1057        }
1058
1059        if control.hovered {
1060            return MouseEvent::Move(curr_pos);
1061        }
1062        MouseEvent::None
1063    }
1064
1065    #[inline(never)]
1066    /// Allocates a widget cell from `Custom` state preferred size and hands rendering control to user code.
1067    pub fn custom_render_widget<F: FnMut(Dimensioni, &CustomRenderArgs) + 'static>(&mut self, state: &mut Custom, f: F) {
1068        let rect = self.next_widget_rect(state);
1069        let opt = *state.widget_opt();
1070        let bopt = *state.behaviour_opt();
1071        let (control, _) = self.run_widget(state, rect, None, opt, bopt);
1072
1073        let input = self.snapshot_input();
1074        let input_ref = input.as_ref();
1075        let mouse_event = self.input_to_mouse_event(&control, input_ref, rect);
1076
1077        let active = control.focused && self.in_hover_root;
1078        let key_mods = if active { input_ref.key_mods } else { KeyMode::NONE };
1079        let key_codes = if active { input_ref.key_codes } else { KeyCode::NONE };
1080        let text_input = if active { input_ref.text_input.clone() } else { String::new() };
1081        let cra = CustomRenderArgs {
1082            content_area: rect,
1083            view: self.get_clip_rect(),
1084            mouse_event,
1085            scroll_delta: control.scroll_delta,
1086            widget_opt: state.opt,
1087            behaviour_opt: state.bopt,
1088            key_mods,
1089            key_codes,
1090            text_input,
1091        };
1092        self.command_list.push(Command::CustomRender(cra, Box::new(f)));
1093    }
1094
1095    widget_layout!(
1096        /// Draws a textbox using the next available layout cell.
1097        textbox,
1098        Textbox,
1099        |this: &mut Self, state: &mut Textbox, rect: Recti| {
1100            let input = Some(this.snapshot_input());
1101            let opt = state.opt | WidgetOption::HOLD_FOCUS;
1102            this.handle_widget_in_rect(state, rect, input, opt, state.bopt)
1103        }
1104    );
1105
1106    widget_layout!(
1107        /// Draws a multi-line text area using the next available layout cell.
1108        textarea,
1109        TextArea,
1110        |this: &mut Self, state: &mut TextArea, rect: Recti| {
1111            let input = Some(this.snapshot_input());
1112            let opt = state.opt | WidgetOption::HOLD_FOCUS;
1113            this.handle_widget_in_rect(state, rect, input, opt, state.bopt)
1114        }
1115    );
1116
1117    widget_layout!(
1118        #[inline(never)]
1119        /// Draws a horizontal slider bound to `state`.
1120        slider,
1121        Slider,
1122        |this: &mut Self, state: &mut Slider, rect: Recti| {
1123            let mut opt = state.opt;
1124            if state.edit.editing {
1125                opt |= WidgetOption::HOLD_FOCUS;
1126            }
1127            let input = Some(this.snapshot_input());
1128            this.handle_widget_in_rect(state, rect, input, opt, state.bopt)
1129        }
1130    );
1131
1132    widget_layout!(
1133        #[inline(never)]
1134        /// Draws a numeric input that can be edited via keyboard or by dragging.
1135        number,
1136        Number,
1137        |this: &mut Self, state: &mut Number, rect: Recti| {
1138            let mut opt = state.opt;
1139            if state.edit.editing {
1140                opt |= WidgetOption::HOLD_FOCUS;
1141            }
1142            let input = Some(this.snapshot_input());
1143            this.handle_widget_in_rect(state, rect, input, opt, state.bopt)
1144        }
1145    );
1146}
1147
1148#[cfg(test)]
1149mod tests {
1150    use super::*;
1151    use crate::{AtlasSource, FontEntry, SourceFormat};
1152
1153    const ICON_NAMES: [&str; 6] = ["white", "close", "expand", "collapse", "check", "expand_down"];
1154
1155    fn make_test_atlas() -> AtlasHandle {
1156        let pixels: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF];
1157        let icons: Vec<(&str, Recti)> = ICON_NAMES.iter().map(|name| (*name, Recti::new(0, 0, 1, 1))).collect();
1158        let entries = vec![
1159            (
1160                '_',
1161                CharEntry {
1162                    offset: Vec2i::new(0, 0),
1163                    advance: Vec2i::new(8, 0),
1164                    rect: Recti::new(0, 0, 1, 1),
1165                },
1166            ),
1167            (
1168                'a',
1169                CharEntry {
1170                    offset: Vec2i::new(0, 0),
1171                    advance: Vec2i::new(8, 0),
1172                    rect: Recti::new(0, 0, 1, 1),
1173                },
1174            ),
1175            (
1176                'b',
1177                CharEntry {
1178                    offset: Vec2i::new(0, 0),
1179                    advance: Vec2i::new(8, 0),
1180                    rect: Recti::new(0, 0, 1, 1),
1181                },
1182            ),
1183        ];
1184        let fonts = vec![(
1185            "default",
1186            FontEntry {
1187                line_size: 10,
1188                baseline: 8,
1189                font_size: 10,
1190                entries: &entries,
1191            },
1192        )];
1193        let source = AtlasSource {
1194            width: 1,
1195            height: 1,
1196            pixels: &pixels,
1197            icons: &icons,
1198            fonts: &fonts,
1199            format: SourceFormat::Raw,
1200            slots: &[],
1201        };
1202        AtlasHandle::from(&source)
1203    }
1204
1205    fn make_container() -> Container {
1206        let atlas = make_test_atlas();
1207        let input = Rc::new(RefCell::new(Input::default()));
1208        let mut container = Container::new("test", atlas, Rc::new(Style::default()), input);
1209        container.in_hover_root = true;
1210        container.push_container_body(rect(0, 0, 100, 30), ContainerOption::NONE, WidgetBehaviourOption::NONE);
1211        container
1212    }
1213
1214    #[test]
1215    fn scrollbars_use_current_body() {
1216        let mut container = make_container();
1217        let mut style = Style::default();
1218        style.padding = 0;
1219        style.scrollbar_size = 10;
1220        container.style = Rc::new(style);
1221
1222        container.body = rect(0, 0, 1, 1);
1223        container.content_size = Vec2i::new(0, 0);
1224
1225        let mut body = rect(0, 0, 100, 100);
1226        container.scrollbars(&mut body);
1227
1228        assert_eq!(body.width, 100);
1229        assert_eq!(body.height, 100);
1230    }
1231
1232    #[test]
1233    fn scrollbars_shrink_body_when_needed() {
1234        let mut container = make_container();
1235        let mut style = Style::default();
1236        style.padding = 0;
1237        style.scrollbar_size = 10;
1238        container.style = Rc::new(style);
1239
1240        container.content_size = Vec2i::new(200, 200);
1241
1242        let mut body = rect(0, 0, 100, 100);
1243        container.scrollbars(&mut body);
1244
1245        assert_eq!(body.width, 90);
1246        assert_eq!(body.height, 90);
1247    }
1248
1249    #[test]
1250    fn textbox_left_moves_over_multibyte() {
1251        let mut container = make_container();
1252        let input = container.input.clone();
1253        let mut state = Textbox::new("a\u{1F600}b");
1254        let textbox_id = widget_id_of(&state);
1255        container.set_focus(Some(textbox_id));
1256        state.cursor = 5;
1257
1258        input.borrow_mut().keydown_code(KeyCode::LEFT);
1259        let rect = container.layout.next();
1260        let control_state = (state.opt | WidgetOption::HOLD_FOCUS, state.bopt);
1261        let control = container.update_control(textbox_id, rect, &control_state);
1262        let input = container.snapshot_input();
1263        let mut ctx = container.widget_ctx(textbox_id, rect, Some(input));
1264        state.handle(&mut ctx, &control);
1265
1266        let cursor = state.cursor;
1267        assert_eq!(cursor, 1);
1268    }
1269
1270    #[test]
1271    fn textbox_backspace_removes_multibyte() {
1272        let mut container = make_container();
1273        let input = container.input.clone();
1274        let mut state = Textbox::new("a\u{1F600}b");
1275        let textbox_id = widget_id_of(&state);
1276        container.set_focus(Some(textbox_id));
1277        state.cursor = 5;
1278
1279        input.borrow_mut().keydown(KeyMode::BACKSPACE);
1280        let rect = container.layout.next();
1281        let control_state = (state.opt | WidgetOption::HOLD_FOCUS, state.bopt);
1282        let control = container.update_control(textbox_id, rect, &control_state);
1283        let input = container.snapshot_input();
1284        let mut ctx = container.widget_ctx(textbox_id, rect, Some(input));
1285        state.handle(&mut ctx, &control);
1286
1287        let cursor = state.cursor;
1288        assert_eq!(state.buf, "ab");
1289        assert_eq!(cursor, 1);
1290    }
1291}