Skip to main content

pane/
build.rs

1//! Code-side widget construction.
2//!
3//! Builders mirror the RON authoring path but produce `(UiItem, ItemState)` pairs
4//! directly, so callers can spawn widgets at runtime via
5//! [`StandaloneHandle::create`](crate::api::StandaloneHandle::create) and friends.
6//!
7//! Style references are by string name and resolved against the running pane's
8//! style map at create time. Pass `None` to fall back to the menu's `default_style`.
9
10use crate::items::{Button, Dropdown, RadioGroup, Slider, TextBox, Toggle, UiItem};
11use crate::loader::{
12    DropdownAction, PressAction, RadioAction, SliderAction, TextBoxAction, ToggleAction,
13};
14use crate::styles::StyleId;
15use crate::widgets::{
16    ButtonState, DropdownState, ItemState, RadioGroupState, SliderState, TextBoxState,
17};
18
19/// Common trait for code-constructed widgets. Used by `create()` on the handles.
20pub trait WidgetBuilder {
21    fn build(self, default_style: Option<StyleId>) -> (UiItem, ItemState);
22}
23
24// ── Button ────────────────────────────────────────────────────────────────────
25
26pub struct ButtonBuilder {
27    id: String,
28    x: f32,
29    y: f32,
30    w: f32,
31    h: f32,
32    text: String,
33    style: Option<StyleId>,
34    tooltip: Option<String>,
35    action: PressAction,
36    disabled: bool,
37}
38
39impl ButtonBuilder {
40    #[must_use]
41    pub fn new(id: impl Into<String>) -> Self {
42        Self {
43            id: id.into(),
44            x: 0.0,
45            y: 0.0,
46            w: 100.0,
47            h: 40.0,
48            text: String::new(),
49            style: None,
50            tooltip: None,
51            action: PressAction::Custom(String::new()),
52            disabled: false,
53        }
54    }
55    #[must_use]
56    pub const fn pos(mut self, x: f32, y: f32) -> Self {
57        self.x = x;
58        self.y = y;
59        self
60    }
61    #[must_use]
62    pub const fn size(mut self, w: f32, h: f32) -> Self {
63        self.w = w;
64        self.h = h;
65        self
66    }
67    #[must_use]
68    pub fn text(mut self, t: impl Into<String>) -> Self {
69        self.text = t.into();
70        self
71    }
72    #[must_use]
73    pub const fn style(mut self, s: StyleId) -> Self {
74        self.style = Some(s);
75        self
76    }
77    #[must_use]
78    pub fn tooltip(mut self, t: impl Into<String>) -> Self {
79        self.tooltip = Some(t.into());
80        self
81    }
82    #[must_use]
83    pub fn on_press(mut self, tag: impl Into<String>) -> Self {
84        self.action = PressAction::Custom(tag.into());
85        self
86    }
87    #[must_use]
88    pub const fn disabled(mut self, d: bool) -> Self {
89        self.disabled = d;
90        self
91    }
92}
93
94impl WidgetBuilder for ButtonBuilder {
95    fn build(self, default_style: Option<StyleId>) -> (UiItem, ItemState) {
96        let style = self.style.or(default_style).unwrap_or(StyleId::new(0));
97        let item = Button {
98            id: self.id,
99            x: self.x,
100            y: self.y,
101            width: self.w,
102            height: self.h,
103            text: self.text,
104            style,
105            tooltip: self.tooltip,
106            action: self.action,
107            disabled: self.disabled,
108            nav_default: false,
109        };
110        (UiItem::Button(item), ItemState::Button(ButtonState::new()))
111    }
112}
113
114// ── Toggle ────────────────────────────────────────────────────────────────────
115
116pub struct ToggleBuilder {
117    id: String,
118    x: f32,
119    y: f32,
120    w: f32,
121    h: f32,
122    text: String,
123    style_off: Option<StyleId>,
124    style_on: Option<StyleId>,
125    tooltip: Option<String>,
126    action: ToggleAction,
127    default_checked: bool,
128    disabled: bool,
129}
130
131impl ToggleBuilder {
132    #[must_use]
133    pub fn new(id: impl Into<String>) -> Self {
134        Self {
135            id: id.into(),
136            x: 0.0,
137            y: 0.0,
138            w: 100.0,
139            h: 40.0,
140            text: String::new(),
141            style_off: None,
142            style_on: None,
143            tooltip: None,
144            action: ToggleAction::Custom(String::new()),
145            default_checked: false,
146            disabled: false,
147        }
148    }
149    #[must_use]
150    pub const fn pos(mut self, x: f32, y: f32) -> Self {
151        self.x = x;
152        self.y = y;
153        self
154    }
155    #[must_use]
156    pub const fn size(mut self, w: f32, h: f32) -> Self {
157        self.w = w;
158        self.h = h;
159        self
160    }
161    #[must_use]
162    pub fn text(mut self, t: impl Into<String>) -> Self {
163        self.text = t.into();
164        self
165    }
166    #[must_use]
167    pub const fn style_off(mut self, s: StyleId) -> Self {
168        self.style_off = Some(s);
169        self
170    }
171    #[must_use]
172    pub const fn style_on(mut self, s: StyleId) -> Self {
173        self.style_on = Some(s);
174        self
175    }
176    #[must_use]
177    pub const fn checked(mut self, c: bool) -> Self {
178        self.default_checked = c;
179        self
180    }
181    #[must_use]
182    pub fn on_change(mut self, tag: impl Into<String>) -> Self {
183        self.action = ToggleAction::Custom(tag.into());
184        self
185    }
186}
187
188impl WidgetBuilder for ToggleBuilder {
189    fn build(self, default_style: Option<StyleId>) -> (UiItem, ItemState) {
190        let fb = default_style.unwrap_or(StyleId::new(0));
191        let item = Toggle {
192            id: self.id,
193            x: self.x,
194            y: self.y,
195            width: self.w,
196            height: self.h,
197            text: self.text,
198            tooltip: self.tooltip,
199            default_checked: self.default_checked,
200            disabled: self.disabled,
201            style_off: self.style_off.unwrap_or(fb),
202            style_on: self.style_on.unwrap_or(fb),
203            action: self.action,
204        };
205        let state = ButtonState::for_toggle(&item);
206        (UiItem::Toggle(item), ItemState::Toggle(state))
207    }
208}
209
210// ── Slider ────────────────────────────────────────────────────────────────────
211
212pub struct SliderBuilder {
213    id: String,
214    x: f32,
215    y: f32,
216    w: f32,
217    h: f32,
218    min: f32,
219    max: f32,
220    default_value: f32,
221    step: Option<f32>,
222    style_track: Option<StyleId>,
223    style_thumb: Option<StyleId>,
224    tooltip: Option<String>,
225    action: SliderAction,
226}
227
228impl SliderBuilder {
229    #[must_use]
230    pub fn new(id: impl Into<String>) -> Self {
231        Self {
232            id: id.into(),
233            x: 0.0,
234            y: 0.0,
235            w: 200.0,
236            h: 40.0,
237            min: 0.0,
238            max: 1.0,
239            default_value: 0.0,
240            step: None,
241            style_track: None,
242            style_thumb: None,
243            tooltip: None,
244            action: SliderAction::Custom(String::new()),
245        }
246    }
247    #[must_use]
248    pub const fn pos(mut self, x: f32, y: f32) -> Self {
249        self.x = x;
250        self.y = y;
251        self
252    }
253    #[must_use]
254    pub const fn size(mut self, w: f32, h: f32) -> Self {
255        self.w = w;
256        self.h = h;
257        self
258    }
259    #[must_use]
260    pub const fn range(mut self, min: f32, max: f32) -> Self {
261        self.min = min;
262        self.max = max;
263        self
264    }
265    #[must_use]
266    pub const fn value(mut self, v: f32) -> Self {
267        self.default_value = v;
268        self
269    }
270    #[must_use]
271    pub const fn step(mut self, s: f32) -> Self {
272        self.step = Some(s);
273        self
274    }
275    #[must_use]
276    pub const fn style_track(mut self, s: StyleId) -> Self {
277        self.style_track = Some(s);
278        self
279    }
280    #[must_use]
281    pub const fn style_thumb(mut self, s: StyleId) -> Self {
282        self.style_thumb = Some(s);
283        self
284    }
285    #[must_use]
286    pub fn on_change(mut self, tag: impl Into<String>) -> Self {
287        self.action = SliderAction::Custom(tag.into());
288        self
289    }
290}
291
292impl WidgetBuilder for SliderBuilder {
293    fn build(self, default_style: Option<StyleId>) -> (UiItem, ItemState) {
294        let fb = default_style.unwrap_or(StyleId::new(0));
295        let item = Slider {
296            id: self.id,
297            x: self.x,
298            y: self.y,
299            width: self.w,
300            height: self.h,
301            min: self.min,
302            max: self.max,
303            default_value: self.default_value.clamp(self.min, self.max),
304            step: self.step,
305            tooltip: self.tooltip,
306            style_track: self.style_track.unwrap_or(fb),
307            style_thumb: self.style_thumb.unwrap_or(fb),
308            action: self.action,
309        };
310        let state = SliderState::for_slider(&item);
311        (UiItem::Slider(item), ItemState::Slider(state))
312    }
313}
314
315// ── TextBox ───────────────────────────────────────────────────────────────────
316
317pub struct TextBoxBuilder {
318    id: String,
319    x: f32,
320    y: f32,
321    w: f32,
322    h: f32,
323    default_text: String,
324    placeholder: String,
325    max_len: Option<usize>,
326    style_idle: Option<StyleId>,
327    style_focus: Option<StyleId>,
328    tooltip: Option<String>,
329    on_change: TextBoxAction,
330    on_submit: TextBoxAction,
331    password: bool,
332    multiline: bool,
333    font_size: Option<f32>,
334}
335
336impl TextBoxBuilder {
337    #[must_use]
338    pub fn new(id: impl Into<String>) -> Self {
339        Self {
340            id: id.into(),
341            x: 0.0,
342            y: 0.0,
343            w: 200.0,
344            h: 40.0,
345            default_text: String::new(),
346            placeholder: String::new(),
347            max_len: None,
348            style_idle: None,
349            style_focus: None,
350            tooltip: None,
351            on_change: TextBoxAction::Custom(String::new()),
352            on_submit: TextBoxAction::Custom(String::new()),
353            password: false,
354            multiline: false,
355            font_size: None,
356        }
357    }
358    #[must_use]
359    pub const fn pos(mut self, x: f32, y: f32) -> Self {
360        self.x = x;
361        self.y = y;
362        self
363    }
364    #[must_use]
365    pub const fn size(mut self, w: f32, h: f32) -> Self {
366        self.w = w;
367        self.h = h;
368        self
369    }
370    #[must_use]
371    pub fn text(mut self, t: impl Into<String>) -> Self {
372        self.default_text = t.into();
373        self
374    }
375    #[must_use]
376    pub fn placeholder(mut self, t: impl Into<String>) -> Self {
377        self.placeholder = t.into();
378        self
379    }
380    #[must_use]
381    pub const fn max_len(mut self, n: usize) -> Self {
382        self.max_len = Some(n);
383        self
384    }
385    #[must_use]
386    pub const fn style(mut self, s: StyleId) -> Self {
387        self.style_idle = Some(s);
388        self
389    }
390    #[must_use]
391    pub const fn style_focus(mut self, s: StyleId) -> Self {
392        self.style_focus = Some(s);
393        self
394    }
395    #[must_use]
396    pub const fn password(mut self, p: bool) -> Self {
397        self.password = p;
398        self
399    }
400    #[must_use]
401    pub const fn multiline(mut self, m: bool) -> Self {
402        self.multiline = m;
403        self
404    }
405    #[must_use]
406    pub fn on_change(mut self, tag: impl Into<String>) -> Self {
407        self.on_change = TextBoxAction::Custom(tag.into());
408        self
409    }
410    #[must_use]
411    pub fn on_submit(mut self, tag: impl Into<String>) -> Self {
412        self.on_submit = TextBoxAction::Custom(tag.into());
413        self
414    }
415}
416
417impl WidgetBuilder for TextBoxBuilder {
418    fn build(self, default_style: Option<StyleId>) -> (UiItem, ItemState) {
419        let fb = default_style.unwrap_or(StyleId::new(0));
420        let style_idle = self.style_idle.unwrap_or(fb);
421        let item = TextBox {
422            id: self.id,
423            x: self.x,
424            y: self.y,
425            width: self.w,
426            height: self.h,
427            default_text: self.default_text,
428            placeholder: self.placeholder,
429            max_len: self.max_len,
430            tooltip: self.tooltip,
431            style_idle,
432            style_focus: self.style_focus.unwrap_or(style_idle),
433            on_change: self.on_change,
434            on_submit: self.on_submit,
435            password: self.password,
436            multiline: self.multiline,
437            font_size: self.font_size,
438        };
439        let state = TextBoxState::for_textbox(&item);
440        (UiItem::TextBox(item), ItemState::TextBox(state))
441    }
442}
443
444// ── Dropdown ──────────────────────────────────────────────────────────────────
445
446pub struct DropdownBuilder {
447    id: String,
448    x: f32,
449    y: f32,
450    w: f32,
451    h: f32,
452    options: Vec<String>,
453    default_selected: usize,
454    style: Option<StyleId>,
455    style_list: Option<StyleId>,
456    style_item: Option<StyleId>,
457    action: DropdownAction,
458}
459
460impl DropdownBuilder {
461    #[must_use]
462    pub fn new(id: impl Into<String>) -> Self {
463        Self {
464            id: id.into(),
465            x: 0.0,
466            y: 0.0,
467            w: 200.0,
468            h: 40.0,
469            options: Vec::new(),
470            default_selected: 0,
471            style: None,
472            style_list: None,
473            style_item: None,
474            action: DropdownAction::Custom(String::new()),
475        }
476    }
477    #[must_use]
478    pub const fn pos(mut self, x: f32, y: f32) -> Self {
479        self.x = x;
480        self.y = y;
481        self
482    }
483    #[must_use]
484    pub const fn size(mut self, w: f32, h: f32) -> Self {
485        self.w = w;
486        self.h = h;
487        self
488    }
489    #[must_use]
490    pub fn options<I, S>(mut self, opts: I) -> Self
491    where
492        I: IntoIterator<Item = S>,
493        S: Into<String>,
494    {
495        self.options = opts.into_iter().map(Into::into).collect();
496        self
497    }
498    #[must_use]
499    pub const fn selected(mut self, i: usize) -> Self {
500        self.default_selected = i;
501        self
502    }
503    #[must_use]
504    pub const fn style(mut self, s: StyleId) -> Self {
505        self.style = Some(s);
506        self
507    }
508    #[must_use]
509    pub fn on_change(mut self, tag: impl Into<String>) -> Self {
510        self.action = DropdownAction::Custom(tag.into());
511        self
512    }
513}
514
515impl WidgetBuilder for DropdownBuilder {
516    fn build(self, default_style: Option<StyleId>) -> (UiItem, ItemState) {
517        let fb = default_style.unwrap_or(StyleId::new(0));
518        let header_style = self.style.unwrap_or(fb);
519        let item_style = self.style_item.unwrap_or(header_style);
520        let n = self.options.len();
521        let opt_h = self.h;
522        let items: Vec<Button> = self
523            .options
524            .iter()
525            .enumerate()
526            .map(|(i, label)| Button {
527                id: format!("{}__opt_{}", self.id, i),
528                x: 0.0,
529                y: (i as f32 + 1.0) * opt_h,
530                width: self.w,
531                height: opt_h,
532                text: label.clone(),
533                style: item_style,
534                tooltip: None,
535                action: PressAction::DropdownSelect {
536                    dropdown_id: self.id.clone(),
537                    index: i,
538                },
539                disabled: false,
540                nav_default: false,
541            })
542            .collect();
543        let item = Dropdown {
544            id: self.id,
545            x: self.x,
546            y: self.y,
547            width: self.w,
548            height: self.h,
549            default_selected: self.default_selected.min(n.saturating_sub(1)),
550            options: self.options,
551            style: header_style,
552            style_list: self.style_list,
553            action: self.action,
554            items,
555        };
556        let state = DropdownState::for_dropdown(&item);
557        (UiItem::Dropdown(item), ItemState::Dropdown(state))
558    }
559}
560
561// ── RadioGroup ────────────────────────────────────────────────────────────────
562
563pub struct RadioGroupBuilder {
564    id: String,
565    x: f32,
566    y: f32,
567    w: f32,
568    h: f32,
569    options: Vec<String>,
570    default_selected: usize,
571    style_idle: Option<StyleId>,
572    style_selected: Option<StyleId>,
573    gap: f32,
574    action: RadioAction,
575}
576
577impl RadioGroupBuilder {
578    #[must_use]
579    pub fn new(id: impl Into<String>) -> Self {
580        Self {
581            id: id.into(),
582            x: 0.0,
583            y: 0.0,
584            w: 200.0,
585            h: 120.0,
586            options: Vec::new(),
587            default_selected: 0,
588            style_idle: None,
589            style_selected: None,
590            gap: 4.0,
591            action: RadioAction::Custom(String::new()),
592        }
593    }
594    #[must_use]
595    pub const fn pos(mut self, x: f32, y: f32) -> Self {
596        self.x = x;
597        self.y = y;
598        self
599    }
600    #[must_use]
601    pub const fn size(mut self, w: f32, h: f32) -> Self {
602        self.w = w;
603        self.h = h;
604        self
605    }
606    #[must_use]
607    pub fn options<I, S>(mut self, opts: I) -> Self
608    where
609        I: IntoIterator<Item = S>,
610        S: Into<String>,
611    {
612        self.options = opts.into_iter().map(Into::into).collect();
613        self
614    }
615    #[must_use]
616    pub const fn selected(mut self, i: usize) -> Self {
617        self.default_selected = i;
618        self
619    }
620    #[must_use]
621    pub const fn style_idle(mut self, s: StyleId) -> Self {
622        self.style_idle = Some(s);
623        self
624    }
625    #[must_use]
626    pub const fn style_selected(mut self, s: StyleId) -> Self {
627        self.style_selected = Some(s);
628        self
629    }
630    #[must_use]
631    pub const fn gap(mut self, g: f32) -> Self {
632        self.gap = g;
633        self
634    }
635    #[must_use]
636    pub fn on_change(mut self, tag: impl Into<String>) -> Self {
637        self.action = RadioAction::Custom(tag.into());
638        self
639    }
640}
641
642impl WidgetBuilder for RadioGroupBuilder {
643    fn build(self, default_style: Option<StyleId>) -> (UiItem, ItemState) {
644        let fb = default_style.unwrap_or(StyleId::new(0));
645        let style_idle = self.style_idle.unwrap_or(fb);
646        let style_selected = self.style_selected.unwrap_or(style_idle);
647        let n = self.options.len();
648        let total_gap = self.gap * n.saturating_sub(1) as f32;
649        let opt_h = if n > 0 {
650            (self.h - total_gap) / n as f32
651        } else {
652            self.h
653        };
654        let selected = self.default_selected.min(n.saturating_sub(1));
655        let items: Vec<Button> = self
656            .options
657            .iter()
658            .enumerate()
659            .map(|(i, label)| Button {
660                id: format!("{}__opt_{}", self.id, i),
661                x: 0.0,
662                y: i as f32 * (opt_h + self.gap),
663                width: self.w,
664                height: opt_h,
665                text: label.clone(),
666                style: if i == selected {
667                    style_selected
668                } else {
669                    style_idle
670                },
671                tooltip: None,
672                action: PressAction::RadioSelect {
673                    group_id: self.id.clone(),
674                    index: i,
675                },
676                disabled: false,
677                nav_default: i == selected,
678            })
679            .collect();
680        let item = RadioGroup {
681            id: self.id,
682            default_selected: selected,
683            options: self.options,
684            style_idle,
685            style_selected,
686            action: self.action,
687            items,
688            x: self.x,
689            y: self.y,
690        };
691        let state = RadioGroupState::for_radio(&item);
692        (UiItem::RadioGroup(item), ItemState::RadioGroup(state))
693    }
694}