Skip to main content

appcui/ui/button/
button.rs

1use crate::prelude::*;
2use crate::ui::button::{events::EventData, Type};
3
4#[CustomControl(overwrite=OnPaint+OnDefaultAction+OnKeyPressed+OnMouseEvent, internal=true)]
5pub struct Button {
6    button_type: Type,
7    caption: Caption,
8    pressed: bool,
9}
10impl Button {
11    /// Creates a new button with the specified caption and layout. The type of the button will be determined from the current theme.
12    ///
13    /// # Examples
14    /// ```rust,no_run
15    /// use appcui::prelude::*;
16    ///
17    /// let mut button = Button::new("Click me!",
18    ///                              LayoutBuilder::new().x(1).y(1).width(15).build());
19    /// ```
20    pub fn new(caption: &str, layout: Layout) -> Self {
21        Self::inner_create(caption, layout, button::Type::Normal, StatusFlags::ThemeType)
22    }
23    /// Creates a new button with the specified caption, layout and flags
24    /// # Examples
25    /// ```rust,no_run
26    /// use appcui::prelude::*;
27    /// let mut button = Button::with_type("Click me!", layout!("x:1,y:1,w:15"), button::Type::Normal);
28    /// ```
29    pub fn with_type(caption: &str, layout: Layout, button_type: Type) -> Self {
30        Self::inner_create(caption, layout, button_type, StatusFlags::None)
31    }
32
33    fn inner_create(caption: &str, layout: Layout, button_type: Type, status: StatusFlags) -> Self {
34        let mut but = Button {
35            base: ControlBase::with_status_flags(layout, StatusFlags::Visible | StatusFlags::Enabled | StatusFlags::AcceptInput | status),
36            caption: Caption::new(caption, ExtractHotKeyMethod::AltPlusKey),
37            button_type,
38            pressed: false,
39        };
40        but.update_bounds_limits();
41        let hotkey = but.caption.hotkey();
42        but.set_hotkey(hotkey);
43        but
44    }
45
46    fn update_bounds_limits(&mut self) {
47        let (min_w, min_h) = match self.button_type {
48            Type::Normal => (4, 2),
49            Type::Flat => (3, 1),
50            Type::Raised => (5, 3),
51        };
52        self.set_size_bounds(min_w, min_h, u16::MAX, min_h);
53    }
54
55    /// Sets the caption of a button. Using `&` in the provided text followed by a letter or a number will automatically assign Alt+**<number|letter>** hotkey to that button.
56    /// # Examples
57    /// ```rust,no_run
58    /// use appcui::prelude::*;
59    /// let mut button = button!("one,x:1,y:1,w:15"); // the caption is `one`
60    /// button.set_caption("&two");   // now the caption is `two` and Alt+T is a hotkey
61    /// button.set_caption("three");  // caption is `three`, and no hot-key
62    /// ```
63    pub fn set_caption(&mut self, caption: &str) {
64        self.caption.set_text(caption, ExtractHotKeyMethod::AltPlusKey);
65        let hotkey = self.caption.hotkey();
66        self.set_hotkey(hotkey);
67    }
68    /// Returns the button caption.
69    pub fn caption(&self) -> &str {
70        self.caption.text()
71    }
72
73    fn paint_normal(&self, surface: &mut Surface, theme: &Theme) {
74        let col_text = match () {
75            _ if !self.is_enabled() => theme.button.regular.text.inactive,
76            _ if self.has_focus() => theme.button.regular.text.focused,
77            _ if self.is_mouse_over() => theme.button.regular.text.hovered,
78            _ => theme.button.regular.text.normal,
79        };
80        let w = self.size().width.saturating_sub(1);
81        let x = (w / 2) as i32;
82        let mut format = TextFormatBuilder::new()
83            .position(x, 0)
84            .attribute(col_text)
85            .align(TextAlignment::Center)
86            .chars_count(self.caption.chars_count() as u16)
87            .wrap_type(WrapType::SingleLineWrap(w as u16))
88            .build();
89
90        if self.caption.has_hotkey() {
91            format.set_hotkey(
92                match () {
93                    _ if !self.is_enabled() => theme.button.regular.hotkey.inactive,
94                    _ if self.has_focus() => theme.button.regular.hotkey.focused,
95                    _ if self.is_mouse_over() => theme.button.regular.hotkey.hovered,
96                    _ => theme.button.regular.hotkey.normal,
97                },
98                self.caption.hotkey_pos().unwrap() as u32,
99            );
100        }
101        if self.pressed {
102            surface.fill_horizontal_line_with_size(1, 0, w, Character::with_attributes(' ', col_text));
103            format.x += 1;
104            surface.write_text(self.caption.text(), &format);
105        } else {
106            surface.fill_horizontal_line_with_size(0, 0, w, Character::with_attributes(' ', col_text));
107            surface.write_text(self.caption.text(), &format);
108            surface.fill_horizontal_line_with_size(
109                1,
110                1,
111                w,
112                Character::with_attributes(SpecialChar::BlockUpperHalf, theme.button.regular.shadow),
113            );
114            surface.write_char(
115                w as i32,
116                0,
117                Character::with_attributes(SpecialChar::BlockLowerHalf, theme.button.regular.shadow),
118            );
119        }
120    }
121    fn paint_flat(&self, surface: &mut Surface, theme: &Theme) {
122        let col_text = match () {
123            _ if !self.is_enabled() => theme.button.regular.text.inactive,
124            _ if self.has_focus() => theme.button.regular.text.focused,
125            _ if self.is_mouse_over() => theme.button.regular.text.hovered,
126            _ => theme.button.regular.text.normal,
127        };
128        let w = self.size().width;
129        let x = (w / 2) as i32;
130        let mut format = TextFormatBuilder::new()
131            .position(x, 0)
132            .attribute(col_text)
133            .align(TextAlignment::Center)
134            .chars_count(self.caption.chars_count() as u16)
135            .wrap_type(WrapType::SingleLineWrap(w as u16))
136            .build();
137
138        if self.caption.has_hotkey() {
139            format.set_hotkey(
140                match () {
141                    _ if !self.is_enabled() => theme.button.regular.hotkey.inactive,
142                    _ if self.has_focus() => theme.button.regular.hotkey.focused,
143                    _ if self.is_mouse_over() => theme.button.regular.hotkey.hovered,
144                    _ => theme.button.regular.hotkey.normal,
145                },
146                self.caption.hotkey_pos().unwrap() as u32,
147            );
148        }
149        surface.clear(Character::with_attributes(' ', col_text));
150        surface.write_text(self.caption.text(), &format);
151    }
152    fn paint_raised(&self, surface: &mut Surface, theme: &Theme) {
153        let enabled = self.is_enabled();
154        let focus = self.has_focus();
155        let col_text = match () {
156            _ if !enabled => theme.button.bevel.text.inactive,
157            _ if self.pressed => theme.button.bevel.text.pressed_or_selected,
158            _ if focus => theme.button.bevel.text.focused,
159            _ if self.is_mouse_over() => theme.button.bevel.text.hovered,
160            _ => theme.button.bevel.text.normal,
161        };
162        let w = self.size().width;
163        let x = (w / 2) as i32 + if self.pressed { 1 } else { 0 };
164        let mut format = TextFormatBuilder::new()
165            .position(x, 1)
166            .attribute(col_text)
167            .align(TextAlignment::Center)
168            .chars_count(self.caption.chars_count() as u16)
169            .wrap_type(WrapType::SingleLineWrap((w as u16).saturating_sub(2)))
170            .build();
171
172        if self.caption.has_hotkey() {
173            format.set_hotkey(
174                match () {
175                    _ if !enabled => theme.button.bevel.hotkey.inactive,
176                    _ if self.pressed => theme.button.bevel.hotkey.pressed_or_selected,
177                    _ if focus => theme.button.bevel.hotkey.focused,
178                    _ if self.is_mouse_over() => theme.button.bevel.hotkey.hovered,
179                    _ => theme.button.bevel.hotkey.normal,
180                },
181                self.caption.hotkey_pos().unwrap() as u32,
182            );
183        }
184        surface.write_text(self.caption.text(), &format);
185        let r = Rect::with_point_and_size(Point::ORIGIN, self.size());
186        if enabled {
187            surface.draw_bevel_rect(
188                r,
189                LineType::SingleRound,
190                theme.button.bevel.dark_margin,
191                theme.button.bevel.light_margin,
192                !self.pressed,
193            );
194        } else {
195            surface.draw_rect(r, LineType::SingleRound, theme.button.bevel.text.inactive);
196        }
197    }
198}
199impl OnDefaultAction for Button {
200    fn on_default_action(&mut self) {
201        self.raise_event(ControlEvent {
202            emitter: self.handle,
203            receiver: self.event_processor,
204            data: ControlEventData::Button(EventData {}),
205        });
206    }
207}
208impl OnKeyPressed for Button {
209    fn on_key_pressed(&mut self, key: Key, _character: char) -> EventProcessStatus {
210        match key.value() {
211            key!("Space") | key!("Enter") => {
212                self.on_default_action();
213                EventProcessStatus::Processed
214            }
215            _ => EventProcessStatus::Ignored,
216        }
217    }
218}
219
220impl OnPaint for Button {
221    fn on_paint(&self, surface: &mut Surface, theme: &Theme) {
222        match self.button_type {
223            Type::Normal => self.paint_normal(surface, theme),
224            Type::Flat => self.paint_flat(surface, theme),
225            Type::Raised => self.paint_raised(surface, theme),
226        }
227    }
228}
229impl OnMouseEvent for Button {
230    fn on_mouse_event(&mut self, event: &MouseEvent) -> EventProcessStatus {
231        match event {
232            MouseEvent::Enter => {
233                if self.caption.chars_count() > (self.size().width - 2) as usize {
234                    self.show_tooltip(self.caption.text());
235                }
236                EventProcessStatus::Processed
237            }
238            MouseEvent::Leave => EventProcessStatus::Processed,
239            MouseEvent::Released(data) => {
240                self.pressed = false;
241                if self.is_coord_in_control(data.x, data.y) {
242                    self.on_default_action();
243                }
244                EventProcessStatus::Processed
245            }
246            MouseEvent::Drag(data) => {
247                if self.pressed && (!self.is_coord_in_control(data.x, data.y)) {
248                    self.pressed = false;
249                    return EventProcessStatus::Processed;
250                }
251                EventProcessStatus::Ignored
252            }
253            MouseEvent::Pressed(_) => {
254                self.pressed = true;
255                EventProcessStatus::Processed
256            }
257            _ => EventProcessStatus::Ignored,
258        }
259    }
260}