Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/ui_components/checkbox.rs
1use crate::color::ColorU;
2use crate::elements::{ChildAnchor, ParentAnchor, ParentOffsetBounds};
3use crate::geometry::vector::Vector2F;
4use crate::prelude::{Coords, Fill};
5use crate::{
6 elements::{
7 Align, Border, ConstrainedBox, Container, Element, Flex, Hoverable, Icon, MouseState,
8 MouseStateHandle, OffsetPositioning, ParentElement, Rect, Stack,
9 },
10 ui_components::components::{UiComponent, UiComponentStyles},
11 ui_components::text::Span,
12};
13use lazy_static::lazy_static;
14 
15const CHECK_SVG_PATH: &str = "bundled/svg/check-thick.svg";
16 
17/// Number of pixels that should be between the checkmark and checkbox.
18const CHECKMARK_LENGTH_ADJUSTMENT: f32 = 3.;
19 
20const LABEL_LEFT_MARGIN: f32 = 4.;
21 
22lazy_static! {
23 pub static ref HOVER_BACKGROUND_COLOR: ColorU = ColorU::new(170, 170, 170, 50);
24}
25 
26pub struct Checkbox {
27 default_styles: UiComponentStyles,
28 hovered_styles: Option<UiComponentStyles>,
29 checked_styles: Option<UiComponentStyles>,
30 disabled_styles: Option<UiComponentStyles>,
31 hover_state: MouseStateHandle,
32 
33 disabled: bool,
34 checked: bool,
35 
36 /// Optional, clickable text rendered to the right of the checkbox
37 label: Option<Span>,
38}
39 
40impl UiComponent for Checkbox {
41 type ElementType = Hoverable;
42 fn build(self) -> Hoverable {
43 let hoverable = Hoverable::new(self.hover_state.clone(), |state| {
44 let checkbox = self.render_checkbox(state);
45 if let Some(label) = self.label.clone() {
46 Flex::row()
47 .with_cross_axis_alignment(crate::elements::CrossAxisAlignment::Center)
48 .with_child(checkbox)
49 .with_child(
50 Container::new(label.with_style(self.styles(state)).build().finish())
51 .with_margin_left(LABEL_LEFT_MARGIN)
52 .finish(),
53 )
54 .finish()
55 } else {
56 checkbox
57 }
58 });
59 if self.disabled {
60 return hoverable.disable();
61 }
62 hoverable
63 }
64 
65 /// Overwrites _some_ styles passed in `style` parameter
66 fn with_style(self, styles: UiComponentStyles) -> Self {
67 Self {
68 default_styles: self.default_styles.merge(styles),
69 hovered_styles: Some(
70 self.hovered_styles
71 .unwrap_or(self.default_styles)
72 .merge(styles),
73 ),
74 checked_styles: Some(
75 self.checked_styles
76 .unwrap_or(self.default_styles)
77 .merge(styles),
78 ),
79 disabled_styles: Some(
80 self.disabled_styles
81 .unwrap_or(self.default_styles)
82 .merge(styles),
83 ),
84 ..self
85 }
86 }
87}
88 
89impl Checkbox {
90 pub fn new(
91 mouse_state: MouseStateHandle,
92 default_styles: UiComponentStyles,
93 hovered_styles: Option<UiComponentStyles>,
94 checked_styles: Option<UiComponentStyles>,
95 disabled_styles: Option<UiComponentStyles>,
96 ) -> Self {
97 Self {
98 default_styles,
99 hovered_styles,
100 checked_styles,
101 disabled_styles,
102 hover_state: mouse_state,
103 disabled: false,
104 checked: false,
105 label: None,
106 }
107 }
108 
109 pub fn disabled(mut self) -> Self {
110 self.disabled = true;
111 self
112 }
113 
114 pub fn check(mut self, check: bool) -> Self {
115 self.checked = check;
116 self
117 }
118 
119 pub fn with_label(mut self, label: Span) -> Self {
120 self.label = Some(label);
121 self
122 }
123 
124 fn styles(&self, state: &MouseState) -> UiComponentStyles {
125 let styles = if self.disabled {
126 self.disabled_styles
127 } else if self.checked || state.is_clicked() {
128 self.checked_styles
129 } else {
130 None
131 };
132 styles.unwrap_or(self.default_styles)
133 }
134 
135 // If checked, use the icon with the appropriate color. Otherwise, use an empty box.
136 fn render_checkmark(&self, checked: bool, icon_color: ColorU) -> Box<dyn Element> {
137 if checked {
138 Icon::new(CHECK_SVG_PATH, icon_color).finish()
139 } else {
140 Rect::new().finish()
141 }
142 }
143 
144 fn render_checkbox(&self, state: &MouseState) -> Box<dyn Element> {
145 let styles = self.styles(state);
146 
147 let border_width = styles.border_width;
148 
149 // The full length of the checkbox will be the font size, but we need
150 // to account for the border on each side of the length.
151 let checkbox_length = styles.font_size.unwrap_or_default();
152 let checkbox_length_without_border = checkbox_length - 2. * border_width.unwrap_or(0.);
153 
154 // Use font_color for the checkmark when checked, otherwise use default foreground
155 let icon_color = styles.font_color.unwrap_or_default();
156 let checkmark = self.render_checkmark(self.checked, icon_color);
157 let checkmark_length = checkbox_length_without_border - CHECKMARK_LENGTH_ADJUSTMENT;
158 
159 let mut checkbox = Container::new(
160 ConstrainedBox::new(
161 Align::new(
162 ConstrainedBox::new(checkmark)
163 .with_height(checkmark_length)
164 .with_width(checkmark_length)
165 .finish(),
166 )
167 .finish(),
168 )
169 .with_width(checkbox_length_without_border)
170 .with_height(checkbox_length_without_border)
171 .finish(),
172 )
173 .with_corner_radius(styles.border_radius.unwrap_or_default());
174 
175 if !state.is_mouse_over_element() {
176 if let Some(background) = styles.background {
177 checkbox = checkbox.with_background(background);
178 }
179 }
180 
181 if let Some(border_width) = border_width {
182 checkbox = checkbox.with_border(
183 Border::all(border_width).with_border_fill(styles.border_color.unwrap_or_default()),
184 );
185 }
186 
187 let mut stack = Stack::new();
188 
189 if state.is_mouse_over_element() {
190 let hover = Container::new(
191 ConstrainedBox::new(
192 Rect::new()
193 .with_background(
194 self.hovered_styles
195 .and_then(|styles| styles.background)
196 .unwrap_or(Fill::Solid(*HOVER_BACKGROUND_COLOR)),
197 )
198 .with_corner_radius(styles.border_radius.unwrap_or_default())
199 .finish(),
200 )
201 .with_width(checkbox_length)
202 .with_height(checkbox_length)
203 .finish(),
204 )
205 .finish();
206 
207 // Position the hover so that it's centered behind the checkbox.
208 stack.add_positioned_child(
209 hover,
210 OffsetPositioning::offset_from_parent(
211 Vector2F::zero(),
212 ParentOffsetBounds::Unbounded,
213 ParentAnchor::Center,
214 ChildAnchor::Center,
215 ),
216 );
217 }
218 
219 // Add the checkbox itself
220 stack.add_child(checkbox.finish());
221 
222 let margin = styles
223 .margin
224 .unwrap_or(Coords::uniform(checkbox_length / 2.));
225 
226 Container::new(stack.finish())
227 .with_margin_left(margin.left)
228 .with_margin_right(margin.right)
229 .with_margin_top(margin.top)
230 .with_margin_bottom(margin.bottom)
231 .finish()
232 }
233}
234