Skip to main content

fui/controls/
button.rs

1use super::internal::button_presenter::{
2    ButtonPresenter, ButtonTemplate, ButtonVisualState, DEFAULT_BUTTON_TEMPLATE,
3};
4use super::*;
5use crate::ffi::CursorStyle;
6use crate::node::{TextNode, WeakFlexBox};
7use crate::theme::subscribe;
8use crate::{focus_adorner, focus_visibility};
9use std::rc::Rc;
10
11#[derive(Clone)]
12pub struct Button {
13    root: FlexBox,
14    presenter: Rc<RefCell<Rc<dyn ButtonPresenter>>>,
15    label: Rc<RefCell<TextNode>>,
16    template: Rc<RefCell<Option<Rc<dyn ButtonTemplate>>>>,
17    label_value: Rc<RefCell<String>>,
18    click: Rc<RefCell<Option<ClickCallback>>>,
19    hovered_state: Rc<Cell<bool>>,
20    pressed_state: Rc<Cell<bool>>,
21    focused_state: Rc<Cell<bool>>,
22    keyboard_armed_key: Rc<RefCell<Option<String>>>,
23    font_family_override: Rc<RefCell<Option<crate::FontFamily>>>,
24    font_size_override: Rc<Cell<Option<f32>>>,
25    text_color_override: Rc<Cell<Option<u32>>>,
26    colors_value: Rc<Cell<Option<ButtonColors>>>,
27}
28
29impl Button {
30    pub fn new(label: impl Into<String>) -> Self {
31        let label = label.into();
32        let root = row();
33        let presenter = create_button_presenter(None);
34        let label_node = presenter.label_node();
35        label_node.text(label.clone());
36        root.interactive(true)
37            .focusable(true, 0)
38            .semantic_role(SemanticRole::Button)
39            .semantic_label(label.clone())
40            .reflect_semantic_disabled_from_enabled()
41            .child(&presenter.content_root());
42
43        let control = Self {
44            root,
45            presenter: Rc::new(RefCell::new(presenter)),
46            label: Rc::new(RefCell::new(label_node)),
47            template: Rc::new(RefCell::new(None)),
48            label_value: Rc::new(RefCell::new(label.clone())),
49            click: Rc::new(RefCell::new(None)),
50            hovered_state: Rc::new(Cell::new(false)),
51            pressed_state: Rc::new(Cell::new(false)),
52            focused_state: Rc::new(Cell::new(false)),
53            keyboard_armed_key: Rc::new(RefCell::new(None)),
54            font_family_override: Rc::new(RefCell::new(None)),
55            font_size_override: Rc::new(Cell::new(None)),
56            text_color_override: Rc::new(Cell::new(None)),
57            colors_value: Rc::new(Cell::new(None)),
58        };
59        control.install_visual_subscriptions();
60        control.sync_visual_state();
61        control.sync_focus_chrome();
62        control.bind_events();
63        control
64    }
65
66    fn bind_events(&self) {
67        let event_target = self.event_target();
68        self.root.on_pointer_enter(move |_event| {
69            event_target.set_hovered(true);
70        });
71        let event_target = self.event_target();
72        self.root.on_pointer_leave(move |_event| {
73            event_target.set_hovered(false);
74            if event_target.keyboard_armed_key.borrow().is_none() {
75                event_target.cancel_press();
76            }
77        });
78        let event_target = self.event_target();
79        self.root.on_pointer_down(move |_event| {
80            event_target.set_hovered(true);
81            event_target.begin_press();
82        });
83        let event_target = self.event_target();
84        self.root.on_pointer_up(move |_event| {
85            if event_target.pressed_state.get()
86                && event_target.keyboard_armed_key.borrow().is_none()
87            {
88                event_target.end_press();
89            }
90        });
91
92        let event_target = self.event_target();
93        self.root.on_pointer_click(move |event| {
94            fire_click_callback(&event_target.click);
95            event.handled = true;
96        });
97        let event_target = self.event_target();
98        self.root.on_key_down(move |event| {
99            event_target.sync_focus_chrome();
100            if !event_target.is_enabled() || event.modifiers != 0 || !is_activation_key(event) {
101                return;
102            }
103            if event_target.keyboard_armed_key.borrow().is_some() {
104                event.handled = true;
105                return;
106            }
107            *event_target.keyboard_armed_key.borrow_mut() = Some(event.key.clone());
108            event_target.begin_press();
109            event.handled = true;
110        });
111        let event_target = self.event_target();
112        self.root.on_key_up(move |event| {
113            if !event_target.is_enabled() || event.modifiers != 0 || !is_activation_key(event) {
114                return;
115            }
116            let armed = event_target.keyboard_armed_key.borrow().clone();
117            if armed.as_deref() == Some(event.key.as_str()) {
118                *event_target.keyboard_armed_key.borrow_mut() = None;
119                event_target.end_press();
120                fire_click_callback(&event_target.click);
121                event.handled = true;
122            }
123        });
124        let event_target = self.event_target();
125        self.root.on_focus_changed(move |event| {
126            if !event.focused && event_target.keyboard_armed_key.borrow().is_some() {
127                *event_target.keyboard_armed_key.borrow_mut() = None;
128                event_target.cancel_press();
129            }
130            if event_target.focused_state.get() != event.focused {
131                event_target.focused_state.set(event.focused);
132                event_target.sync_visual_state();
133                event_target.sync_focus_chrome();
134            }
135        });
136    }
137
138    pub fn text(&self, value: impl Into<String>) -> &Self {
139        let value = value.into();
140        self.label_value.replace(value.clone());
141        self.label.borrow().text(value.clone());
142        self.root.semantic_label(value);
143        self
144    }
145
146    pub fn template(&self, template: Rc<dyn ButtonTemplate>) -> &Self {
147        self.set_template(Some(template))
148    }
149
150    pub fn clear_template(&self) -> &Self {
151        self.set_template(None)
152    }
153
154    fn set_template(&self, template: Option<Rc<dyn ButtonTemplate>>) -> &Self {
155        self.template.replace(template.clone());
156        self.replace_presenter(create_button_presenter(template));
157        self
158    }
159
160    pub fn colors(&self, colors: ButtonColors) -> &Self {
161        self.set_colors(Some(colors))
162    }
163
164    pub fn clear_colors(&self) -> &Self {
165        self.set_colors(None)
166    }
167
168    fn set_colors(&self, colors: Option<ButtonColors>) -> &Self {
169        self.colors_value.set(colors);
170        self.sync_visual_state();
171        self
172    }
173
174    pub fn enabled(&self, enabled: bool) -> &Self {
175        self.root.enabled(enabled);
176        if !enabled {
177            self.hovered_state.set(false);
178            self.focused_state.set(false);
179            *self.keyboard_armed_key.borrow_mut() = None;
180            self.cancel_press();
181        }
182        self.sync_visual_state();
183        self.sync_focus_chrome();
184        self
185    }
186
187    fn set_explicit_text_color(&self, color: u32) {
188        self.text_color_override.set(Some(color));
189        self.sync_visual_state();
190    }
191
192    fn set_explicit_font_family(&self, family: crate::FontFamily) {
193        self.font_family_override.replace(Some(family.clone()));
194        self.label.borrow().font_family(family);
195    }
196
197    fn set_explicit_font_size(&self, size: f32) {
198        self.font_size_override.set(Some(size));
199        self.label.borrow().font_size(size);
200    }
201
202    fn install_visual_subscriptions(&self) {
203        let event_target = self.event_target();
204        let theme_guard = subscribe(move |_theme| {
205            event_target.sync_visual_state();
206            event_target.sync_focus_chrome();
207        });
208        self.root
209            .retained_node_ref()
210            .retain_attachment(Rc::new(theme_guard));
211
212        let event_target = self.event_target();
213        let focus_guard = focus_visibility::subscribe(move |_visible| {
214            event_target.sync_focus_chrome();
215        });
216        self.root
217            .retained_node_ref()
218            .retain_attachment(Rc::new(focus_guard));
219    }
220
221    fn event_target(&self) -> ButtonEventTarget {
222        ButtonEventTarget {
223            weak_root: self.root.downgrade(),
224            presenter: self.presenter.clone(),
225            label: self.label.clone(),
226            template: self.template.clone(),
227            label_value: self.label_value.clone(),
228            click: self.click.clone(),
229            hovered_state: self.hovered_state.clone(),
230            pressed_state: self.pressed_state.clone(),
231            focused_state: self.focused_state.clone(),
232            keyboard_armed_key: self.keyboard_armed_key.clone(),
233            font_family_override: self.font_family_override.clone(),
234            font_size_override: self.font_size_override.clone(),
235            text_color_override: self.text_color_override.clone(),
236            colors_value: self.colors_value.clone(),
237        }
238    }
239
240    pub(crate) fn is_enabled(&self) -> bool {
241        self.root.retained_node_ref().is_enabled_for_routing()
242    }
243
244    pub(crate) fn begin_press(&self) {
245        self.pressed_state.set(true);
246        self.sync_visual_state();
247        self.sync_focus_chrome();
248    }
249
250    pub(crate) fn end_press(&self, activate: bool) {
251        self.pressed_state.set(false);
252        self.sync_visual_state();
253        self.sync_focus_chrome();
254        if activate {
255            fire_click_callback(&self.click);
256        }
257    }
258
259    pub(crate) fn cancel_press(&self) {
260        if self.pressed_state.replace(false) {
261            self.sync_visual_state();
262        }
263        self.sync_focus_chrome();
264    }
265
266    fn sync_visual_state(&self) {
267        sync_button_visual_state(
268            &self.root,
269            &self.presenter,
270            self.hovered_state.get(),
271            self.pressed_state.get(),
272            self.focused_state.get(),
273            self.is_enabled(),
274            self.font_family_override.borrow().clone(),
275            self.font_size_override.get(),
276            self.text_color_override.get(),
277            self.colors_value.get(),
278        );
279    }
280
281    fn sync_focus_chrome(&self) {
282        sync_button_focus_chrome(&self.root, self.focused_state.get(), self.is_enabled());
283    }
284
285    fn replace_presenter(&self, next_presenter: Rc<dyn ButtonPresenter>) {
286        let previous_presenter = self.presenter.borrow().clone();
287        if Rc::ptr_eq(&previous_presenter, &next_presenter) {
288            return;
289        }
290        next_presenter
291            .label_node()
292            .text(self.label_value.borrow().clone());
293        self.root.child(&next_presenter.content_root());
294        self.root.remove_child(&previous_presenter.content_root());
295        previous_presenter.content_root().dispose();
296        self.label.replace(next_presenter.label_node());
297        self.presenter.replace(next_presenter);
298        self.sync_visual_state();
299        self.sync_focus_chrome();
300    }
301}
302
303impl Clickable for Button {
304    fn on_click(&self, handler: impl Fn(ClickEventArgs) + 'static) -> &Self {
305        *self.click.borrow_mut() = Some(Rc::new(handler));
306        self
307    }
308}
309
310impl Node for Button {
311    fn retained_node_ref(&self) -> NodeRef {
312        self.root.retained_node_ref()
313    }
314
315    fn build_self(&self) {
316        self.sync_visual_state();
317        self.sync_focus_chrome();
318        self.root.build_self();
319    }
320}
321
322impl HasFlexBoxRoot for Button {
323    fn flex_box_root(&self) -> &FlexBox {
324        &self.root
325    }
326}
327
328impl ThemeBindable for Button {
329    fn theme_binding_node(&self) -> NodeRef {
330        self.root.retained_node_ref()
331    }
332
333    fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
334        let target = self.event_target();
335        Box::new(move || target.upgrade())
336    }
337}
338
339impl LabeledControlTextStyle for Button {
340    fn set_label_font_family(&self, family: crate::FontFamily) {
341        self.set_explicit_font_family(family);
342    }
343
344    fn set_label_font_size(&self, size: f32) {
345        self.set_explicit_font_size(size);
346    }
347
348    fn set_label_text_color(&self, color: u32) {
349        self.set_explicit_text_color(color);
350    }
351}
352
353#[allow(clippy::too_many_arguments)]
354fn sync_button_visual_state(
355    root: &FlexBox,
356    presenter: &Rc<RefCell<Rc<dyn ButtonPresenter>>>,
357    hovered: bool,
358    pressed: bool,
359    focused: bool,
360    enabled: bool,
361    font_family_override: Option<crate::FontFamily>,
362    font_size_override: Option<f32>,
363    text_color_override: Option<u32>,
364    colors: Option<ButtonColors>,
365) {
366    let presenter = presenter.borrow().clone();
367    let theme = current_theme();
368    let state = ButtonVisualState {
369        hovered,
370        pressed,
371        focused,
372        enabled,
373    };
374    let mut host_style = presenter.present(&theme, &state, colors.as_ref());
375    host_style.cursor = Some(if enabled {
376        CursorStyle::Pointer
377    } else {
378        CursorStyle::Default
379    });
380    host_style.opacity = Some(if enabled { 1.0 } else { 0.38 });
381    root.apply_presenter_style(host_style);
382    if let Some(family) = font_family_override {
383        presenter.label_node().font_family(family);
384    }
385    if let Some(size) = font_size_override {
386        presenter.label_node().font_size(size);
387    }
388    if let Some(color) = text_color_override {
389        presenter.label_node().text_color(color);
390    }
391}
392
393fn sync_button_focus_chrome(root: &FlexBox, focused: bool, enabled: bool) {
394    if focused && enabled && focus_visibility::keyboard_focus_visible() {
395        let corners = root
396            .resolved_host_style()
397            .corners
398            .unwrap_or_else(|| crate::Corners::all(current_theme().spacing.sm));
399        focus_adorner::show_standard_corners(
400            root,
401            corners.top_left,
402            corners.top_right,
403            corners.bottom_right,
404            corners.bottom_left,
405        );
406        return;
407    }
408    focus_adorner::hide_owner(root);
409}
410
411fn create_button_presenter(template: Option<Rc<dyn ButtonTemplate>>) -> Rc<dyn ButtonPresenter> {
412    if let Some(template) = template {
413        return template.create();
414    }
415    if let Some(template_set) = get_control_templates() {
416        if let Some(template) = template_set.button {
417            return template.create();
418        }
419    }
420    DEFAULT_BUTTON_TEMPLATE.create()
421}
422
423#[derive(Clone)]
424struct ButtonEventTarget {
425    weak_root: WeakFlexBox,
426    presenter: Rc<RefCell<Rc<dyn ButtonPresenter>>>,
427    label: Rc<RefCell<TextNode>>,
428    template: Rc<RefCell<Option<Rc<dyn ButtonTemplate>>>>,
429    label_value: Rc<RefCell<String>>,
430    click: Rc<RefCell<Option<ClickCallback>>>,
431    hovered_state: Rc<Cell<bool>>,
432    pressed_state: Rc<Cell<bool>>,
433    focused_state: Rc<Cell<bool>>,
434    keyboard_armed_key: Rc<RefCell<Option<String>>>,
435    font_family_override: Rc<RefCell<Option<crate::FontFamily>>>,
436    font_size_override: Rc<Cell<Option<f32>>>,
437    text_color_override: Rc<Cell<Option<u32>>>,
438    colors_value: Rc<Cell<Option<ButtonColors>>>,
439}
440
441impl ButtonEventTarget {
442    fn upgrade(&self) -> Option<Button> {
443        Some(Button {
444            root: self.weak_root.upgrade()?,
445            presenter: self.presenter.clone(),
446            label: self.label.clone(),
447            template: self.template.clone(),
448            label_value: self.label_value.clone(),
449            click: self.click.clone(),
450            hovered_state: self.hovered_state.clone(),
451            pressed_state: self.pressed_state.clone(),
452            focused_state: self.focused_state.clone(),
453            keyboard_armed_key: self.keyboard_armed_key.clone(),
454            font_family_override: self.font_family_override.clone(),
455            font_size_override: self.font_size_override.clone(),
456            text_color_override: self.text_color_override.clone(),
457            colors_value: self.colors_value.clone(),
458        })
459    }
460
461    fn is_enabled(&self) -> bool {
462        self.weak_root
463            .upgrade()
464            .is_some_and(|root| root.retained_node_ref().is_enabled_for_routing())
465    }
466
467    fn sync_visual_state(&self) {
468        let Some(root) = self.weak_root.upgrade() else {
469            return;
470        };
471        sync_button_visual_state(
472            &root,
473            &self.presenter,
474            self.hovered_state.get(),
475            self.pressed_state.get(),
476            self.focused_state.get(),
477            root.retained_node_ref().is_enabled_for_routing(),
478            self.font_family_override.borrow().clone(),
479            self.font_size_override.get(),
480            self.text_color_override.get(),
481            self.colors_value.get(),
482        );
483    }
484
485    fn sync_focus_chrome(&self) {
486        let Some(root) = self.weak_root.upgrade() else {
487            return;
488        };
489        sync_button_focus_chrome(
490            &root,
491            self.focused_state.get(),
492            root.retained_node_ref().is_enabled_for_routing(),
493        );
494    }
495
496    fn begin_press(&self) {
497        self.pressed_state.set(true);
498        self.sync_visual_state();
499        self.sync_focus_chrome();
500    }
501
502    fn end_press(&self) {
503        self.pressed_state.set(false);
504        self.sync_visual_state();
505        self.sync_focus_chrome();
506    }
507
508    fn cancel_press(&self) {
509        if self.pressed_state.replace(false) {
510            self.sync_visual_state();
511        }
512        self.sync_focus_chrome();
513    }
514
515    fn set_hovered(&self, hovered: bool) {
516        if self.hovered_state.replace(hovered) != hovered {
517            self.sync_visual_state();
518        }
519        self.sync_focus_chrome();
520    }
521}