microui_redux/
widgets.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 std::fmt::Write;
56use std::rc::Rc;
57
58/// Shared context passed to widget handlers.
59pub struct WidgetCtx<'a> {
60    id: Id,
61    rect: Recti,
62    draw: DrawCtx<'a>,
63    focus: &'a mut Option<Id>,
64    updated_focus: &'a mut bool,
65    in_hover_root: bool,
66    input: Option<Rc<InputSnapshot>>,
67}
68
69impl<'a> WidgetCtx<'a> {
70    /// Creates a widget context for the given widget ID and rectangle.
71    pub(crate) fn new(
72        id: Id,
73        rect: Recti,
74        commands: &'a mut Vec<Command>,
75        clip_stack: &'a mut Vec<Recti>,
76        style: &'a Style,
77        atlas: &'a AtlasHandle,
78        focus: &'a mut Option<Id>,
79        updated_focus: &'a mut bool,
80        in_hover_root: bool,
81        input: Option<Rc<InputSnapshot>>,
82    ) -> Self {
83        Self {
84            id,
85            rect,
86            draw: DrawCtx::new(commands, clip_stack, style, atlas),
87            focus,
88            updated_focus,
89            in_hover_root,
90            input,
91        }
92    }
93
94    /// Returns the widget identifier.
95    pub fn id(&self) -> Id { self.id }
96
97    /// Returns the widget rectangle.
98    pub fn rect(&self) -> Recti { self.rect }
99
100    /// Returns the input snapshot for this widget, if provided.
101    pub fn input(&self) -> Option<&InputSnapshot> { self.input.as_deref() }
102
103    /// Sets focus to this widget for the current frame.
104    pub fn set_focus(&mut self) {
105        *self.focus = Some(self.id);
106        *self.updated_focus = true;
107    }
108
109    /// Clears focus from the current widget.
110    pub fn clear_focus(&mut self) {
111        *self.focus = None;
112        *self.updated_focus = true;
113    }
114
115    /// Pushes a new clip rectangle onto the stack.
116    pub fn push_clip_rect(&mut self, rect: Recti) { self.draw.push_clip_rect(rect); }
117
118    /// Pops the current clip rectangle.
119    pub fn pop_clip_rect(&mut self) { self.draw.pop_clip_rect(); }
120
121    /// Executes `f` with the provided clip rect applied.
122    pub fn with_clip<F: FnOnce(&mut Self)>(&mut self, rect: Recti, f: F) {
123        self.push_clip_rect(rect);
124        f(self);
125        self.pop_clip_rect();
126    }
127
128    fn current_clip_rect(&self) -> Recti { self.draw.current_clip_rect() }
129
130    pub(crate) fn style(&self) -> &Style { self.draw.style() }
131
132    pub(crate) fn atlas(&self) -> &AtlasHandle { self.draw.atlas() }
133
134    /// Pushes a raw draw command into the command buffer.
135    pub fn push_command(&mut self, cmd: Command) { self.draw.push_command(cmd); }
136
137    /// Sets the current clip rectangle for subsequent draw commands.
138    pub fn set_clip(&mut self, rect: Recti) { self.draw.set_clip(rect); }
139
140    /// Returns the clipping relation between `r` and the current clip rect.
141    pub fn check_clip(&self, r: Recti) -> Clip { self.draw.check_clip(r) }
142
143    pub(crate) fn draw_rect(&mut self, rect: Recti, color: Color) { self.draw.draw_rect(rect, color); }
144
145    /// Draws a 1-pixel box outline using the supplied color.
146    pub fn draw_box(&mut self, r: Recti, color: Color) { self.draw.draw_box(r, color); }
147
148    pub(crate) fn draw_text(&mut self, font: FontId, text: &str, pos: Vec2i, color: Color) {
149        self.draw.draw_text(font, text, pos, color);
150    }
151
152    pub(crate) fn draw_icon(&mut self, id: IconId, rect: Recti, color: Color) { self.draw.draw_icon(id, rect, color); }
153
154    pub(crate) fn push_image(&mut self, image: Image, rect: Recti, color: Color) { self.draw.push_image(image, rect, color); }
155
156    pub(crate) fn draw_slot_with_function(&mut self, id: SlotId, rect: Recti, color: Color, f: Rc<dyn Fn(usize, usize) -> Color4b>) {
157        self.draw.draw_slot_with_function(id, rect, color, f);
158    }
159
160    pub(crate) fn draw_frame(&mut self, rect: Recti, colorid: ControlColor) { self.draw.draw_frame(rect, colorid); }
161
162    pub(crate) fn draw_widget_frame(&mut self, control: &ControlState, rect: Recti, colorid: ControlColor, opt: WidgetOption) {
163        self.draw.draw_widget_frame(control.focused, control.hovered, rect, colorid, opt);
164    }
165
166    pub(crate) fn draw_control_text(&mut self, text: &str, rect: Recti, colorid: ControlColor, opt: WidgetOption) {
167        self.draw.draw_control_text(text, rect, colorid, opt);
168    }
169
170    pub(crate) fn mouse_over(&self, rect: Recti) -> bool {
171        let input = match self.input.as_ref() {
172            Some(input) => input,
173            None => return false,
174        };
175        if !self.in_hover_root {
176            return false;
177        }
178        let clip_rect = self.current_clip_rect();
179        rect.contains(&input.mouse_pos) && clip_rect.contains(&input.mouse_pos)
180    }
181}
182
183#[derive(Clone, Copy)]
184/// Expansion state used by tree nodes, headers, and similar widgets.
185pub enum NodeStateValue {
186    /// Child content is visible.
187    Expanded,
188    /// Child content is hidden.
189    Closed,
190}
191
192impl NodeStateValue {
193    /// Returns `true` when the node is expanded.
194    pub fn is_expanded(&self) -> bool {
195        match self {
196            Self::Expanded => true,
197            _ => false,
198        }
199    }
200
201    /// Returns `true` when the node is closed.
202    pub fn is_closed(&self) -> bool {
203        match self {
204            Self::Closed => true,
205            _ => false,
206        }
207    }
208}
209
210#[derive(Clone)]
211/// Persistent state for headers and tree nodes.
212pub struct Node {
213    /// Label displayed for the node.
214    pub label: String,
215    /// Current expansion state.
216    pub state: NodeStateValue,
217    /// Widget options applied to the node.
218    pub opt: WidgetOption,
219    /// Behaviour options applied to the node.
220    pub bopt: WidgetBehaviourOption,
221}
222
223impl Node {
224    /// Creates a node state with the default widget options.
225    pub fn new(label: impl Into<String>, state: NodeStateValue) -> Self {
226        Self { label: label.into(), state, opt: WidgetOption::NONE, bopt: WidgetBehaviourOption::NONE }
227    }
228
229    /// Creates a node state with explicit widget options.
230    pub fn with_opt(label: impl Into<String>, state: NodeStateValue, opt: WidgetOption) -> Self {
231        Self { label: label.into(), state, opt, bopt: WidgetBehaviourOption::NONE }
232    }
233
234    /// Returns `true` when the node is expanded.
235    pub fn is_expanded(&self) -> bool { self.state.is_expanded() }
236
237    /// Returns `true` when the node is closed.
238    pub fn is_closed(&self) -> bool { self.state.is_closed() }
239}
240
241impl Widget for Node {
242    fn widget_opt(&self) -> &WidgetOption { &self.opt }
243    fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
244    fn handle(&mut self, _ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
245        if control.clicked {
246            self.state = if self.state.is_expanded() { NodeStateValue::Closed } else { NodeStateValue::Expanded };
247            ResourceState::CHANGE
248        } else {
249            ResourceState::NONE
250        }
251    }
252}
253
254fn widget_fill_color(control: &ControlState, base: ControlColor, fill: WidgetFillOption) -> Option<ControlColor> {
255    if control.focused && fill.fill_click() {
256        let mut color = base;
257        color.focus();
258        Some(color)
259    } else if control.hovered && fill.fill_hover() {
260        let mut color = base;
261        color.hover();
262        Some(color)
263    } else if fill.fill_normal() {
264        Some(base)
265    } else {
266        None
267    }
268}
269
270#[derive(Clone)]
271/// Describes the content rendered inside a button widget.
272pub enum ButtonContent {
273    /// A text label and optional icon from the atlas.
274    Text {
275        /// Text displayed on the button.
276        label: String,
277        /// Optional icon rendered on the button.
278        icon: Option<IconId>,
279    },
280    /// A text label and optional image.
281    Image {
282        /// Text displayed on the button.
283        label: String,
284        /// Optional image rendered on the button.
285        image: Option<Image>,
286    },
287    /// A text label and a slot refreshed via a paint callback.
288    Slot {
289        /// Text displayed on the button.
290        label: String,
291        /// Slot rendered on the button.
292        slot: SlotId,
293        /// Callback used to fill the slot pixels.
294        paint: Rc<dyn Fn(usize, usize) -> Color4b>,
295    },
296}
297
298#[derive(Clone)]
299/// Persistent state for button widgets.
300pub struct Button {
301    /// Content rendered inside the button.
302    pub content: ButtonContent,
303    /// Widget options applied to the button.
304    pub opt: WidgetOption,
305    /// Behaviour options applied to the button.
306    pub bopt: WidgetBehaviourOption,
307    /// Fill behavior for the button background.
308    pub fill: WidgetFillOption,
309}
310
311impl Button {
312    /// Creates a text button with default options.
313    pub fn new(label: impl Into<String>) -> Self {
314        Self {
315            content: ButtonContent::Text { label: label.into(), icon: None },
316            opt: WidgetOption::NONE,
317            bopt: WidgetBehaviourOption::NONE,
318            fill: WidgetFillOption::ALL,
319        }
320    }
321
322    /// Creates a text button with explicit widget options.
323    pub fn with_opt(label: impl Into<String>, opt: WidgetOption) -> Self {
324        Self {
325            content: ButtonContent::Text { label: label.into(), icon: None },
326            opt,
327            bopt: WidgetBehaviourOption::NONE,
328            fill: WidgetFillOption::ALL,
329        }
330    }
331
332    /// Creates an image button with explicit widget options and fill behavior.
333    pub fn with_image(label: impl Into<String>, image: Option<Image>, opt: WidgetOption, fill: WidgetFillOption) -> Self {
334        Self {
335            content: ButtonContent::Image { label: label.into(), image },
336            opt,
337            bopt: WidgetBehaviourOption::NONE,
338            fill,
339        }
340    }
341
342    /// Creates a slot button that repaints via the provided callback.
343    pub fn with_slot(
344        label: impl Into<String>,
345        slot: SlotId,
346        paint: Rc<dyn Fn(usize, usize) -> Color4b>,
347        opt: WidgetOption,
348        fill: WidgetFillOption,
349    ) -> Self {
350        Self {
351            content: ButtonContent::Slot { label: label.into(), slot, paint },
352            opt,
353            bopt: WidgetBehaviourOption::NONE,
354            fill,
355        }
356    }
357}
358
359impl Widget for Button {
360    fn widget_opt(&self) -> &WidgetOption { &self.opt }
361    fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
362    fn handle(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
363        let mut res = ResourceState::NONE;
364        if control.clicked {
365            res |= ResourceState::SUBMIT;
366        }
367        let rect = ctx.rect();
368        if !self.opt.has_no_frame() {
369            if let Some(colorid) = widget_fill_color(control, ControlColor::Button, self.fill) {
370                ctx.draw_frame(rect, colorid);
371            }
372        }
373        match &self.content {
374            ButtonContent::Text { label, icon } => {
375                if !label.is_empty() {
376                    ctx.draw_control_text(label, rect, ControlColor::Text, self.opt);
377                }
378                if let Some(icon) = icon {
379                    let color = ctx.style().colors[ControlColor::Text as usize];
380                    ctx.draw_icon(*icon, rect, color);
381                }
382            }
383            ButtonContent::Image { label, image } => {
384                if !label.is_empty() {
385                    ctx.draw_control_text(label, rect, ControlColor::Text, self.opt);
386                }
387                if let Some(image) = *image {
388                    let color = ctx.style().colors[ControlColor::Text as usize];
389                    ctx.push_image(image, rect, color);
390                }
391            }
392            ButtonContent::Slot { label, slot, paint } => {
393                if !label.is_empty() {
394                    ctx.draw_control_text(label, rect, ControlColor::Text, self.opt);
395                }
396                let color = ctx.style().colors[ControlColor::Text as usize];
397                ctx.draw_slot_with_function(*slot, rect, color, paint.clone());
398            }
399        }
400        res
401    }
402}
403
404#[derive(Clone)]
405/// Persistent state for list items.
406pub struct ListItem {
407    /// Label displayed for the list item.
408    pub label: String,
409    /// Optional atlas icon rendered alongside the label.
410    pub icon: Option<IconId>,
411    /// Widget options applied to the list item.
412    pub opt: WidgetOption,
413    /// Behaviour options applied to the list item.
414    pub bopt: WidgetBehaviourOption,
415}
416
417impl ListItem {
418    /// Creates a list item with default widget options.
419    pub fn new(label: impl Into<String>) -> Self {
420        Self { label: label.into(), icon: None, opt: WidgetOption::NONE, bopt: WidgetBehaviourOption::NONE }
421    }
422
423    /// Creates a list item with explicit widget options.
424    pub fn with_opt(label: impl Into<String>, opt: WidgetOption) -> Self {
425        Self { label: label.into(), icon: None, opt, bopt: WidgetBehaviourOption::NONE }
426    }
427
428    /// Creates a list item with an icon and default widget options.
429    pub fn with_icon(label: impl Into<String>, icon: IconId) -> Self {
430        Self { label: label.into(), icon: Some(icon), opt: WidgetOption::NONE, bopt: WidgetBehaviourOption::NONE }
431    }
432
433    /// Creates a list item with an icon and explicit widget options.
434    pub fn with_icon_opt(label: impl Into<String>, icon: IconId, opt: WidgetOption) -> Self {
435        Self { label: label.into(), icon: Some(icon), opt, bopt: WidgetBehaviourOption::NONE }
436    }
437}
438
439impl Widget for ListItem {
440    fn widget_opt(&self) -> &WidgetOption { &self.opt }
441    fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
442    fn handle(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
443        let mut res = ResourceState::NONE;
444        let bounds = ctx.rect();
445        if control.clicked {
446            res |= ResourceState::SUBMIT;
447        }
448
449        if control.focused || control.hovered {
450            let mut color = ControlColor::Button;
451            if control.focused {
452                color.focus();
453            } else {
454                color.hover();
455            }
456            let fill = ctx.style().colors[color as usize];
457            ctx.draw_rect(bounds, fill);
458        }
459
460        let mut text_rect = bounds;
461        if let Some(icon) = self.icon {
462            let padding = ctx.style().padding.max(0);
463            let icon_size = ctx.atlas().get_icon_size(icon);
464            let icon_x = bounds.x + padding;
465            let icon_y = bounds.y + ((bounds.height - icon_size.height) / 2).max(0);
466            let icon_rect = rect(icon_x, icon_y, icon_size.width, icon_size.height);
467            let consumed = icon_size.width + padding * 2;
468            text_rect.x += consumed;
469            text_rect.width = (text_rect.width - consumed).max(0);
470            let color = ctx.style().colors[ControlColor::Text as usize];
471            ctx.draw_icon(icon, icon_rect, color);
472        }
473
474        if !self.label.is_empty() {
475            ctx.draw_control_text(&self.label, text_rect, ControlColor::Text, self.opt);
476        }
477        res
478    }
479}
480
481#[derive(Clone)]
482/// Persistent state for list boxes.
483pub struct ListBox {
484    /// Label displayed for the list box.
485    pub label: String,
486    /// Optional image rendered alongside the label.
487    pub image: Option<Image>,
488    /// Widget options applied to the list box.
489    pub opt: WidgetOption,
490    /// Behaviour options applied to the list box.
491    pub bopt: WidgetBehaviourOption,
492}
493
494impl ListBox {
495    /// Creates a list box with default widget options.
496    pub fn new(label: impl Into<String>, image: Option<Image>) -> Self {
497        Self { label: label.into(), image, opt: WidgetOption::NONE, bopt: WidgetBehaviourOption::NONE }
498    }
499
500    /// Creates a list box with explicit widget options.
501    pub fn with_opt(label: impl Into<String>, image: Option<Image>, opt: WidgetOption) -> Self {
502        Self { label: label.into(), image, opt, bopt: WidgetBehaviourOption::NONE }
503    }
504}
505
506impl Widget for ListBox {
507    fn widget_opt(&self) -> &WidgetOption { &self.opt }
508    fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
509    fn handle(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
510        let mut res = ResourceState::NONE;
511        let rect = ctx.rect();
512        if control.clicked {
513            res |= ResourceState::SUBMIT;
514        }
515        if !self.opt.has_no_frame() {
516            if let Some(colorid) = widget_fill_color(control, ControlColor::Button, WidgetFillOption::HOVER | WidgetFillOption::CLICK) {
517                ctx.draw_frame(rect, colorid);
518            }
519        }
520        if !self.label.is_empty() {
521            ctx.draw_control_text(&self.label, rect, ControlColor::Text, self.opt);
522        }
523        if let Some(image) = self.image {
524            let color = ctx.style().colors[ControlColor::Text as usize];
525            ctx.push_image(image, rect, color);
526        }
527        res
528    }
529}
530
531#[derive(Clone)]
532/// Persistent state for checkbox widgets.
533pub struct Checkbox {
534    /// Label displayed for the checkbox.
535    pub label: String,
536    /// Current value of the checkbox.
537    pub value: bool,
538    /// Widget options applied to the checkbox.
539    pub opt: WidgetOption,
540    /// Behaviour options applied to the checkbox.
541    pub bopt: WidgetBehaviourOption,
542}
543
544impl Checkbox {
545    /// Creates a checkbox with default widget options.
546    pub fn new(label: impl Into<String>, value: bool) -> Self {
547        Self { label: label.into(), value, opt: WidgetOption::NONE, bopt: WidgetBehaviourOption::NONE }
548    }
549
550    /// Creates a checkbox with explicit widget options.
551    pub fn with_opt(label: impl Into<String>, value: bool, opt: WidgetOption) -> Self {
552        Self { label: label.into(), value, opt, bopt: WidgetBehaviourOption::NONE }
553    }
554}
555
556impl Widget for Checkbox {
557    fn widget_opt(&self) -> &WidgetOption { &self.opt }
558    fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
559    fn handle(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
560        let mut res = ResourceState::NONE;
561        let bounds = ctx.rect();
562        let box_rect = rect(bounds.x, bounds.y, bounds.height, bounds.height);
563        if control.clicked {
564            res |= ResourceState::CHANGE;
565            self.value = !self.value;
566        }
567        ctx.draw_widget_frame(control, box_rect, ControlColor::Base, self.opt);
568        if self.value {
569            let color = ctx.style().colors[ControlColor::Text as usize];
570            ctx.draw_icon(CHECK_ICON, box_rect, color);
571        }
572        let text_rect = rect(bounds.x + box_rect.width, bounds.y, bounds.width - box_rect.width, bounds.height);
573        if !self.label.is_empty() {
574            ctx.draw_control_text(&self.label, text_rect, ControlColor::Text, self.opt);
575        }
576        res
577    }
578}
579
580#[derive(Clone)]
581/// Persistent state for textbox widgets.
582pub struct Textbox {
583    /// Buffer edited by the textbox.
584    pub buf: String,
585    /// Current cursor position within the buffer (byte index).
586    pub cursor: usize,
587    /// Widget options applied to the textbox.
588    pub opt: WidgetOption,
589    /// Behaviour options applied to the textbox.
590    pub bopt: WidgetBehaviourOption,
591}
592
593impl Textbox {
594    /// Creates a textbox with default widget options.
595    pub fn new(buf: impl Into<String>) -> Self {
596        let buf = buf.into();
597        let cursor = buf.len();
598        Self { buf, cursor, opt: WidgetOption::NONE, bopt: WidgetBehaviourOption::NONE }
599    }
600
601    /// Creates a textbox with explicit widget options.
602    pub fn with_opt(buf: impl Into<String>, opt: WidgetOption) -> Self {
603        let buf = buf.into();
604        let cursor = buf.len();
605        Self { buf, cursor, opt, bopt: WidgetBehaviourOption::NONE }
606    }
607}
608
609fn textbox_handle(
610    ctx: &mut WidgetCtx<'_>,
611    control: &ControlState,
612    buf: &mut String,
613    cursor: &mut usize,
614    opt: WidgetOption,
615) -> ResourceState {
616    let mut res = ResourceState::NONE;
617    let r = ctx.rect();
618    if !control.focused {
619        *cursor = buf.len();
620    }
621    let mut cursor_pos = (*cursor).min(buf.len());
622
623    let (mouse_pressed, mouse_pos, should_submit) = {
624        let default_input = InputSnapshot::default();
625        let input = ctx.input().unwrap_or(&default_input);
626        let mut should_submit = false;
627
628        if control.focused {
629            if !input.text_input.is_empty() {
630                let insert_at = cursor_pos.min(buf.len());
631                buf.insert_str(insert_at, input.text_input.as_str());
632                cursor_pos = insert_at + input.text_input.len();
633                res |= ResourceState::CHANGE;
634            }
635
636            if input.key_pressed.is_backspace() && cursor_pos > 0 && !buf.is_empty() {
637                let mut new_cursor = cursor_pos.min(buf.len());
638                new_cursor -= 1;
639                while new_cursor > 0 && !buf.is_char_boundary(new_cursor) {
640                    new_cursor -= 1;
641                }
642                buf.replace_range(new_cursor..cursor_pos, "");
643                cursor_pos = new_cursor;
644                res |= ResourceState::CHANGE;
645            }
646
647            if input.key_code_pressed.is_left() && cursor_pos > 0 {
648                let mut new_cursor = cursor_pos - 1;
649                while new_cursor > 0 && !buf.is_char_boundary(new_cursor) {
650                    new_cursor -= 1;
651                }
652                cursor_pos = new_cursor;
653            }
654
655            if input.key_code_pressed.is_right() && cursor_pos < buf.len() {
656                let mut new_cursor = cursor_pos + 1;
657                while new_cursor < buf.len() && !buf.is_char_boundary(new_cursor) {
658                    new_cursor += 1;
659                }
660                cursor_pos = new_cursor;
661            }
662
663            if input.key_pressed.is_return() {
664                should_submit = true;
665            }
666        }
667
668        (input.mouse_pressed, input.mouse_pos, should_submit)
669    };
670
671    if should_submit {
672        ctx.clear_focus();
673        res |= ResourceState::SUBMIT;
674    }
675
676    ctx.draw_widget_frame(control, r, ControlColor::Base, opt);
677
678    let font = ctx.style().font;
679    let line_height = ctx.atlas().get_font_height(font) as i32;
680    let baseline = ctx.atlas().get_font_baseline(font);
681    let descent = (line_height - baseline).max(0);
682
683    let mut texty = r.y + r.height / 2 - line_height / 2;
684    if texty < r.y {
685        texty = r.y;
686    }
687    let max_texty = (r.y + r.height - line_height).max(r.y);
688    if texty > max_texty {
689        texty = max_texty;
690    }
691    let baseline_y = texty + line_height - descent;
692
693    let text_metrics = ctx.atlas().get_text_size(font, buf.as_str());
694    let padding = ctx.style().padding;
695    let ofx = r.width - padding - text_metrics.width - 1;
696    let textx = r.x + if ofx < padding { ofx } else { padding };
697
698    if control.focused && mouse_pressed.is_left() && ctx.mouse_over(r) {
699        let click_x = mouse_pos.x - textx;
700        if click_x <= 0 {
701            cursor_pos = 0;
702        } else {
703            let mut last_width = 0;
704            let mut new_cursor = buf.len();
705            for (idx, ch) in buf.char_indices() {
706                let next = idx + ch.len_utf8();
707                let width = ctx.atlas().get_text_size(font, &buf[..next]).width;
708                if click_x < width {
709                    if click_x < (last_width + width) / 2 {
710                        new_cursor = idx;
711                    } else {
712                        new_cursor = next;
713                    }
714                    break;
715                }
716                last_width = width;
717            }
718            cursor_pos = new_cursor.min(buf.len());
719        }
720    }
721
722    cursor_pos = cursor_pos.min(buf.len());
723    *cursor = cursor_pos;
724
725    let caret_offset = if cursor_pos == 0 {
726        0
727    } else {
728        ctx.atlas().get_text_size(font, &buf[..cursor_pos]).width
729    };
730
731    if control.focused {
732        let color = ctx.style().colors[ControlColor::Text as usize];
733        ctx.push_clip_rect(r);
734        ctx.draw_text(font, buf.as_str(), vec2(textx, texty), color);
735        let caret_top = (baseline_y - baseline + 2).max(r.y).min(r.y + r.height);
736        let caret_bottom = (baseline_y + descent - 2).max(r.y).min(r.y + r.height);
737        let caret_height = (caret_bottom - caret_top).max(1);
738        ctx.draw_rect(rect(textx + caret_offset, caret_top, 1, caret_height), color);
739        ctx.pop_clip_rect();
740    } else {
741        ctx.draw_control_text(buf.as_str(), r, ControlColor::Text, opt);
742    }
743    res
744}
745
746impl Widget for Textbox {
747    fn widget_opt(&self) -> &WidgetOption { &self.opt }
748    fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
749    fn handle(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
750        textbox_handle(ctx, control, &mut self.buf, &mut self.cursor, self.opt)
751    }
752}
753
754#[derive(Clone)]
755/// Persistent state for slider widgets.
756pub struct Slider {
757    /// Current slider value.
758    pub value: Real,
759    /// Lower bound of the slider range.
760    pub low: Real,
761    /// Upper bound of the slider range.
762    pub high: Real,
763    /// Step size used for snapping (0 for continuous).
764    pub step: Real,
765    /// Number of digits after the decimal point when rendering.
766    pub precision: usize,
767    /// Widget options applied to the slider.
768    pub opt: WidgetOption,
769    /// Behaviour options applied to the slider.
770    pub bopt: WidgetBehaviourOption,
771    /// Text editing state for shift-click numeric entry.
772    pub edit: NumberEditState,
773}
774
775impl Slider {
776    /// Creates a slider with default widget options.
777    pub fn new(value: Real, low: Real, high: Real) -> Self {
778        Self {
779            value,
780            low,
781            high,
782            step: 0.0,
783            precision: 0,
784            opt: WidgetOption::NONE,
785            bopt: WidgetBehaviourOption::GRAB_SCROLL,
786            edit: NumberEditState::default(),
787        }
788    }
789
790    /// Creates a slider with explicit widget options.
791    pub fn with_opt(value: Real, low: Real, high: Real, step: Real, precision: usize, opt: WidgetOption) -> Self {
792        Self {
793            value,
794            low,
795            high,
796            step,
797            precision,
798            opt,
799            bopt: WidgetBehaviourOption::GRAB_SCROLL,
800            edit: NumberEditState::default(),
801        }
802    }
803}
804
805fn number_textbox_handle(
806    ctx: &mut WidgetCtx<'_>,
807    control: &ControlState,
808    edit: &mut NumberEditState,
809    precision: usize,
810    value: &mut Real,
811) -> ResourceState {
812    let shift_click = {
813        let default_input = InputSnapshot::default();
814        let input = ctx.input().unwrap_or(&default_input);
815        input.mouse_pressed.is_left() && input.key_mods.is_shift() && control.hovered
816    };
817
818    if shift_click {
819        edit.editing = true;
820        edit.buf.clear();
821        let _ = write!(edit.buf, "{:.*}", precision, value);
822        edit.cursor = edit.buf.len();
823    }
824
825    if edit.editing {
826        let res = textbox_handle(ctx, control, &mut edit.buf, &mut edit.cursor, WidgetOption::NONE);
827        if res.is_submitted() || !control.focused {
828            if let Ok(v) = edit.buf.parse::<f32>() {
829                *value = v as Real;
830            }
831            edit.editing = false;
832            edit.cursor = 0;
833        } else {
834            return ResourceState::ACTIVE;
835        }
836    }
837    ResourceState::NONE
838}
839
840impl Widget for Slider {
841    fn widget_opt(&self) -> &WidgetOption { &self.opt }
842    fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
843    fn handle(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
844        let mut res = ResourceState::NONE;
845        let base = ctx.rect();
846        let last = self.value;
847        let mut v = last;
848        if !number_textbox_handle(ctx, control, &mut self.edit, self.precision, &mut v).is_none() {
849            return res;
850        }
851        if let Some(delta) = control.scroll_delta {
852            let range = self.high - self.low;
853            if range != 0.0 {
854                let wheel = if delta.y != 0 { delta.y.signum() } else { delta.x.signum() };
855                if wheel != 0 {
856                    let step_amount = if self.step != 0. { self.step } else { range / 100.0 };
857                    v += wheel as Real * step_amount;
858                    if self.step != 0. {
859                        v = (v + self.step / 2 as Real) / self.step * self.step;
860                    }
861                }
862            }
863        }
864        let default_input = InputSnapshot::default();
865        let input = ctx.input().unwrap_or(&default_input);
866        let range = self.high - self.low;
867        if control.focused && (!input.mouse_down.is_none() || input.mouse_pressed.is_left()) && base.width > 0 && range != 0.0 {
868            v = self.low + (input.mouse_pos.x - base.x) as Real * range / base.width as Real;
869            if self.step != 0. {
870                v = (v + self.step / 2 as Real) / self.step * self.step;
871            }
872        }
873        if range == 0.0 {
874            v = self.low;
875        }
876        v = if self.high < (if self.low > v { self.low } else { v }) {
877            self.high
878        } else if self.low > v {
879            self.low
880        } else {
881            v
882        };
883        self.value = v;
884        if last != v {
885            res |= ResourceState::CHANGE;
886        }
887        ctx.draw_widget_frame(control, base, ControlColor::Base, self.opt);
888        let w = ctx.style().thumb_size;
889        let available = (base.width - w).max(0);
890        let x = if range != 0.0 && available > 0 {
891            ((v - self.low) * available as Real / range) as i32
892        } else {
893            0
894        };
895        let thumb = rect(base.x + x, base.y, w, base.height);
896        ctx.draw_widget_frame(control, thumb, ControlColor::Button, self.opt);
897        self.edit.buf.clear();
898        let _ = write!(self.edit.buf, "{:.*}", self.precision, self.value);
899        ctx.draw_control_text(self.edit.buf.as_str(), base, ControlColor::Text, self.opt);
900        res
901    }
902}
903
904#[derive(Clone)]
905/// Persistent state for number input widgets.
906pub struct Number {
907    /// Current number value.
908    pub value: Real,
909    /// Step applied when dragging.
910    pub step: Real,
911    /// Number of digits after the decimal point when rendering.
912    pub precision: usize,
913    /// Widget options applied to the number input.
914    pub opt: WidgetOption,
915    /// Behaviour options applied to the number input.
916    pub bopt: WidgetBehaviourOption,
917    /// Text editing state for shift-click numeric entry.
918    pub edit: NumberEditState,
919}
920
921#[derive(Clone, Default)]
922/// Editing buffer for number-style widgets.
923pub struct NumberEditState {
924    /// Whether the widget is currently in edit mode.
925    pub editing: bool,
926    /// Text buffer for numeric input.
927    pub buf: String,
928    /// Cursor position within the buffer (byte index).
929    pub cursor: usize,
930}
931
932impl Number {
933    /// Creates a number input with default widget options.
934    pub fn new(value: Real, step: Real, precision: usize) -> Self {
935        Self {
936            value,
937            step,
938            precision,
939            opt: WidgetOption::NONE,
940            bopt: WidgetBehaviourOption::NONE,
941            edit: NumberEditState::default(),
942        }
943    }
944
945    /// Creates a number input with explicit widget options.
946    pub fn with_opt(value: Real, step: Real, precision: usize, opt: WidgetOption) -> Self {
947        Self {
948            value,
949            step,
950            precision,
951            opt,
952            bopt: WidgetBehaviourOption::NONE,
953            edit: NumberEditState::default(),
954        }
955    }
956}
957
958impl Widget for Number {
959    fn widget_opt(&self) -> &WidgetOption { &self.opt }
960    fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
961    fn handle(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
962        let mut res = ResourceState::NONE;
963        let base = ctx.rect();
964        let last = self.value;
965        if !number_textbox_handle(ctx, control, &mut self.edit, self.precision, &mut self.value).is_none() {
966            return res;
967        }
968        let default_input = InputSnapshot::default();
969        let input = ctx.input().unwrap_or(&default_input);
970        if control.focused && input.mouse_down.is_left() {
971            self.value += input.mouse_delta.x as Real * self.step;
972        }
973        if self.value != last {
974            res |= ResourceState::CHANGE;
975        }
976        ctx.draw_widget_frame(control, base, ControlColor::Base, self.opt);
977        self.edit.buf.clear();
978        let _ = write!(self.edit.buf, "{:.*}", self.precision, self.value);
979        ctx.draw_control_text(self.edit.buf.as_str(), base, ControlColor::Text, self.opt);
980        res
981    }
982}
983
984#[derive(Clone)]
985/// Persistent state for custom render widgets.
986pub struct Custom {
987    /// Label used for debugging or inspection.
988    pub name: String,
989    /// Widget options applied to the custom widget.
990    pub opt: WidgetOption,
991    /// Behaviour options applied to the custom widget.
992    pub bopt: WidgetBehaviourOption,
993}
994
995impl Custom {
996    /// Creates a custom widget state with default options.
997    pub fn new(name: impl Into<String>) -> Self {
998        Self {
999            name: name.into(),
1000            opt: WidgetOption::NONE,
1001            bopt: WidgetBehaviourOption::NONE,
1002        }
1003    }
1004
1005    /// Creates a custom widget state with explicit options.
1006    pub fn with_opt(name: impl Into<String>, opt: WidgetOption, bopt: WidgetBehaviourOption) -> Self {
1007        Self { name: name.into(), opt, bopt }
1008    }
1009}
1010
1011impl Widget for Custom {
1012    fn widget_opt(&self) -> &WidgetOption { &self.opt }
1013    fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
1014    fn handle(&mut self, _ctx: &mut WidgetCtx<'_>, _control: &ControlState) -> ResourceState { ResourceState::NONE }
1015}
1016
1017#[derive(Clone)]
1018/// Persistent state for internal window/container controls.
1019pub struct Internal {
1020    /// Stable tag describing the internal control.
1021    pub tag: &'static str,
1022    /// Widget options applied to the internal control.
1023    pub opt: WidgetOption,
1024    /// Behaviour options applied to the internal control.
1025    pub bopt: WidgetBehaviourOption,
1026}
1027
1028impl Internal {
1029    /// Creates an internal control state with a stable tag.
1030    pub fn new(tag: &'static str) -> Self {
1031        Self {
1032            tag,
1033            opt: WidgetOption::NONE,
1034            bopt: WidgetBehaviourOption::NONE,
1035        }
1036    }
1037}
1038
1039impl Widget for Internal {
1040    fn widget_opt(&self) -> &WidgetOption { &self.opt }
1041    fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
1042    fn handle(&mut self, _ctx: &mut WidgetCtx<'_>, _control: &ControlState) -> ResourceState { ResourceState::NONE }
1043}
1044
1045/// Persistent state used by `combo_box` to track popup and selection.
1046#[derive(Clone)]
1047pub struct Combo {
1048    /// Popup window backing the dropdown list.
1049    pub popup: WindowHandle,
1050    /// Currently selected item index.
1051    pub selected: usize,
1052    /// Whether the combo popup should be open.
1053    pub open: bool,
1054    /// Widget options applied to the combo header.
1055    pub opt: WidgetOption,
1056    /// Behaviour options applied to the combo header.
1057    pub bopt: WidgetBehaviourOption,
1058}
1059
1060impl Combo {
1061    /// Creates a new combo state with the provided popup handle.
1062    pub fn new(popup: WindowHandle) -> Self {
1063        Self { popup, selected: 0, open: false, opt: WidgetOption::NONE, bopt: WidgetBehaviourOption::NONE }
1064    }
1065
1066    /// Creates a new combo state with explicit widget options.
1067    pub fn with_opt(popup: WindowHandle, opt: WidgetOption, bopt: WidgetBehaviourOption) -> Self {
1068        Self { popup, selected: 0, open: false, opt, bopt }
1069    }
1070}
1071
1072impl Widget for Combo {
1073    fn widget_opt(&self) -> &WidgetOption { &self.opt }
1074    fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
1075    fn handle(&mut self, _ctx: &mut WidgetCtx<'_>, _control: &ControlState) -> ResourceState { ResourceState::NONE }
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080    use super::*;
1081    use crate::{AtlasSource, FontEntry, SourceFormat};
1082
1083    const ICON_NAMES: [&str; 6] = ["white", "close", "expand", "collapse", "check", "expand_down"];
1084
1085    fn make_test_atlas() -> AtlasHandle {
1086        let pixels: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF];
1087        let icons: Vec<(&str, Recti)> = ICON_NAMES
1088            .iter()
1089            .map(|name| (*name, Recti::new(0, 0, 1, 1)))
1090            .collect();
1091        let entries = vec![
1092            (
1093                '_',
1094                CharEntry {
1095                    offset: Vec2i::new(0, 0),
1096                    advance: Vec2i::new(8, 0),
1097                    rect: Recti::new(0, 0, 1, 1),
1098                },
1099            ),
1100            (
1101                'a',
1102                CharEntry {
1103                    offset: Vec2i::new(0, 0),
1104                    advance: Vec2i::new(8, 0),
1105                    rect: Recti::new(0, 0, 1, 1),
1106                },
1107            ),
1108            (
1109                'b',
1110                CharEntry {
1111                    offset: Vec2i::new(0, 0),
1112                    advance: Vec2i::new(8, 0),
1113                    rect: Recti::new(0, 0, 1, 1),
1114                },
1115            ),
1116        ];
1117        let fonts = vec![(
1118            "default",
1119            FontEntry {
1120                line_size: 10,
1121                baseline: 8,
1122                font_size: 10,
1123                entries: &entries,
1124            },
1125        )];
1126        let source = AtlasSource {
1127            width: 1,
1128            height: 1,
1129            pixels: &pixels,
1130            icons: &icons,
1131            fonts: &fonts,
1132            format: SourceFormat::Raw,
1133            slots: &[],
1134        };
1135        AtlasHandle::from(&source)
1136    }
1137
1138    #[test]
1139    fn slider_zero_range_keeps_value() {
1140        let atlas = make_test_atlas();
1141        let style = Style::default();
1142        let mut commands = Vec::new();
1143        let mut clip_stack = Vec::new();
1144        let mut focus = None;
1145        let mut updated_focus = false;
1146
1147        let mut slider = Slider::new(5.0, 5.0, 5.0);
1148        let id = slider.get_id();
1149        let rect = rect(0, 0, 100, 20);
1150        let text_input = String::new();
1151        let input = Rc::new(InputSnapshot {
1152            mouse_pos: vec2(50, 10),
1153            mouse_delta: vec2(5, 0),
1154            mouse_down: MouseButton::LEFT,
1155            mouse_pressed: MouseButton::LEFT,
1156            text_input,
1157            ..Default::default()
1158        });
1159        let mut ctx = WidgetCtx::new(
1160            id,
1161            rect,
1162            &mut commands,
1163            &mut clip_stack,
1164            &style,
1165            &atlas,
1166            &mut focus,
1167            &mut updated_focus,
1168            true,
1169            Some(input),
1170        );
1171        let control = ControlState {
1172            hovered: true,
1173            focused: true,
1174            clicked: false,
1175            active: true,
1176            scroll_delta: None,
1177        };
1178
1179        let res = slider.handle(&mut ctx, &control);
1180
1181        assert!(res.is_none());
1182        assert!(slider.value.is_finite());
1183        assert_eq!(slider.value, 5.0);
1184    }
1185}