Skip to main content

recovery_ui/
button.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::constants::constants::*;
6use crate::font;
7use carnelian::color::Color;
8use carnelian::drawing::measure_text_width;
9use carnelian::input::{self};
10use carnelian::scene::facets::{
11    Facet, FacetId, RiveFacet, SetColorMessage, TextFacetOptions, TextHorizontalAlignment,
12    TextVerticalAlignment,
13};
14use carnelian::scene::layout::{
15    Alignment, CrossAxisAlignment, Flex, FlexOptions, MainAxisAlignment, MainAxisSize, Stack,
16    StackOptions,
17};
18use carnelian::scene::scene::{Scene, SceneBuilder};
19use carnelian::{make_message, Coord, Point, Size, ViewAssistantContext};
20use derivative::Derivative;
21use euclid::{size2, Size2D, UnknownUnit};
22use std::ops::Add;
23use zx::MonotonicInstant;
24
25/// enum that defines all messages sent with `App::queue_message` that
26/// the button view assistant will understand and process.
27pub enum ButtonMessages {
28    Pressed(MonotonicInstant, String),
29}
30
31// These values and the text and padding sizes are used to calculate the corners.
32#[derive(Debug, Clone, Copy)]
33#[allow(unused)]
34pub enum ButtonShape {
35    Square = 0,
36    Rounded = 15,
37    Oval = 50,
38}
39
40impl Default for ButtonShape {
41    fn default() -> Self {
42        ButtonShape::Square
43    }
44}
45
46#[derive(Debug, Derivative)]
47#[derivative(Default)]
48pub struct ButtonOptions {
49    #[derivative(Default(value = "SMALL_BUTTON_FONT_SIZE"))]
50    pub font_size: f32,
51    #[derivative(Default(value = "BUTTON_BORDER"))]
52    pub padding: f32,
53    // Whether the background and foreground colours should be swapped
54    #[derivative(Default(value = "false"))]
55    pub bg_fg_swapped: bool,
56    #[derivative(Default(value = "BUTTON_COLOR"))]
57    pub bg_color: Color,
58    #[derivative(Default(value = "ACTIVE_BUTTON_COLOR"))]
59    pub bg_color_active: Color,
60    #[derivative(Default(value = "ACTIVE_BUTTON_COLOR_SWAPPED"))]
61    pub bg_color_active_if_bg_fg_swapped: Color,
62    #[derivative(Default(value = "BUTTON_DISABLED_COLOR"))]
63    pub bg_color_disabled: Color,
64    #[derivative(Default(value = "BUTTON_TEXT_COLOR"))]
65    pub fg_color: Color,
66    #[derivative(Default(value = "BUTTON_TEXT_COLOR"))]
67    pub fg_color_disabled: Color,
68    #[derivative(Default(value = "BUTTON_BORDER_COLOR"))]
69    pub border_color: Color,
70    #[derivative(Default(value = "false"))]
71    pub draw_border: bool,
72    #[derivative(Default(value = "ButtonShape::Square"))]
73    pub shape: ButtonShape,
74    #[derivative(Default(value = "None"))]
75    pub bg_size: Option<Size2D<f32, UnknownUnit>>,
76    #[derivative(Default(value = "None"))]
77    pub text_alignment: Option<Alignment>,
78    #[derivative(Default(value = "false"))]
79    pub hide_text: bool,
80}
81
82pub struct Button {
83    tracking_pointer: Option<input::pointer::PointerId>,
84    active: bool,
85    focused: bool,
86    label_text: String,
87    background: FacetId,
88    label: FacetId,
89    button_options: ButtonOptions,
90}
91
92impl Button {
93    pub fn new(
94        text: &str,
95        icon: Option<RiveFacet>,
96        mut button_options: ButtonOptions,
97        builder: &mut SceneBuilder,
98    ) -> Button {
99        if button_options.bg_fg_swapped {
100            std::mem::swap(&mut button_options.fg_color, &mut button_options.bg_color);
101            button_options.bg_color_active = button_options.bg_color_active_if_bg_fg_swapped;
102        }
103        let mut alignment = match button_options.bg_size {
104            Some(_) => Alignment::center_left(),
105            _ => Alignment::center(),
106        };
107        alignment = button_options.text_alignment.unwrap_or(alignment);
108        let options = StackOptions { alignment, ..StackOptions::default() };
109
110        builder.start_group("button", Stack::with_options_ptr(options));
111        let font_size = button_options.font_size;
112        let (label_text, mut label_width) = if button_options.hide_text || font_size == 0.0 {
113            ("", 0.0)
114        } else {
115            (text, measure_text_width(font::get_default_font_face(), font_size, text))
116        };
117
118        builder.start_group(
119            &("Button icon text row"),
120            Flex::with_options_ptr(FlexOptions::row(
121                MainAxisSize::Min,
122                MainAxisAlignment::Start,
123                CrossAxisAlignment::Start,
124            )),
125        );
126        if let Some(icon) = icon {
127            label_width += icon.calculate_size(size2(0.0, 0.0)).width;
128            builder.facet(Box::new(icon));
129        }
130        builder.space(size2(5.0, 1.0));
131        let label = builder.text(
132            font::get_default_font_face().clone(),
133            label_text,
134            font_size,
135            Point::zero(),
136            TextFacetOptions {
137                color: Color::blue(),
138                horizontal_alignment: TextHorizontalAlignment::Right,
139                vertical_alignment: TextVerticalAlignment::Center,
140                ..TextFacetOptions::default()
141            },
142        );
143        builder.end_group(); // Button icon text row
144
145        let padding = button_options.padding;
146        let bg_size = button_options
147            .bg_size
148            .unwrap_or_else(|| size2(label_width + padding * 2.0, font_size + padding * 2.0));
149        let corner: Coord =
150            Coord::from(bg_size.height * (button_options.shape.clone() as i32 as f32) / 100.0);
151        let background =
152            builder.rounded_rectangle(bg_size.clone(), corner, button_options.bg_color.clone());
153        if button_options.draw_border {
154            let bg_size = bg_size.add(size2(2.0, 2.0));
155            builder.rounded_rectangle(bg_size, corner, button_options.border_color);
156        }
157        builder.end_group(); // button
158
159        Button {
160            button_options,
161            tracking_pointer: None,
162            active: false,
163            focused: false,
164            label_text: text.to_string(),
165            background,
166            label,
167        }
168    }
169
170    #[allow(unused)]
171    pub fn get_size(&self, scene: &mut Scene) -> Size {
172        scene.get_facet_size(&self.background)
173    }
174
175    fn update_button_bg_color(&mut self, scene: &mut Scene) {
176        let (label_color, color) = if self.focused {
177            if self.active {
178                (self.button_options.fg_color, self.button_options.bg_color_active)
179            } else {
180                (self.button_options.fg_color, self.button_options.bg_color)
181            }
182        } else {
183            (self.button_options.fg_color_disabled, self.button_options.bg_color_disabled)
184        };
185        scene.send_message(&self.background, Box::new(SetColorMessage { color }));
186        scene.send_message(&self.label, Box::new(SetColorMessage { color: label_color }));
187    }
188
189    fn set_active(&mut self, scene: &mut Scene, active: bool) {
190        if self.active != active {
191            self.active = active;
192            self.update_button_bg_color(scene);
193        }
194    }
195
196    pub fn set_focused(&mut self, scene: &mut Scene, focused: bool) {
197        if focused != self.focused {
198            self.focused = focused;
199            self.active = false;
200            self.update_button_bg_color(scene);
201            if !focused {
202                self.tracking_pointer = None;
203            }
204        }
205    }
206
207    pub fn handle_pointer_event(
208        &mut self,
209        scene: &mut Scene,
210        context: &mut ViewAssistantContext,
211        pointer_event: &input::pointer::Event,
212    ) {
213        if !self.focused {
214            return;
215        }
216
217        let bounds = scene.get_facet_bounds(&self.background);
218
219        if self.tracking_pointer.is_none() {
220            match pointer_event.phase {
221                input::pointer::Phase::Down(location) => {
222                    self.set_active(scene, bounds.contains(location.to_f32()));
223                    if self.active {
224                        #[cfg(feature = "debug_logging")]
225                        println!("====== Button {} is now active", self.label_text);
226                        self.tracking_pointer = Some(pointer_event.pointer_id.clone());
227                    }
228                }
229                _ => (),
230            }
231        } else {
232            if let Some(tracking_pointer) = self.tracking_pointer.as_ref() {
233                if tracking_pointer == &pointer_event.pointer_id {
234                    match pointer_event.phase {
235                        input::pointer::Phase::Moved(location) => {
236                            self.set_active(scene, bounds.contains(location.to_f32()));
237                        }
238                        input::pointer::Phase::Up => {
239                            if self.active {
240                                #[cfg(feature = "debug_logging")]
241                                println!("====== Button {} pressed", self.label_text);
242                                context.queue_message(make_message(ButtonMessages::Pressed(
243                                    MonotonicInstant::get(),
244                                    self.label_text.clone(),
245                                )));
246                            }
247                            self.tracking_pointer = None;
248                            self.set_active(scene, false);
249                        }
250                        input::pointer::Phase::Remove => {
251                            self.set_active(scene, false);
252                            self.tracking_pointer = None;
253                        }
254                        input::pointer::Phase::Cancel => {
255                            self.set_active(scene, false);
256                            self.tracking_pointer = None;
257                        }
258                        _ => (),
259                    }
260                }
261            }
262        }
263    }
264}
265
266pub trait SceneBuilderButtonExt {
267    fn button(
268        &mut self,
269        text: &str,
270        icon: Option<RiveFacet>,
271        button_options: ButtonOptions,
272    ) -> Button;
273}
274
275impl SceneBuilderButtonExt for SceneBuilder {
276    fn button(
277        &mut self,
278        text: &str,
279        icon: Option<RiveFacet>,
280        button_options: ButtonOptions,
281    ) -> Button {
282        Button::new(text, icon, button_options, self)
283    }
284}