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/button.rs
1use pathfinder_geometry::vector::vec2f;
2use std::borrow::Cow;
3 
4use crate::elements::{
5 Align, ChildAnchor, CrossAxisAlignment, Flex, MainAxisAlignment, MainAxisSize,
6 OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Shrinkable, Stack,
7};
8use crate::geometry::vector::Vector2F;
9 
10use crate::platform::Cursor;
11use crate::{
12 elements::{
13 Border, ConstrainedBox, Container, Element, Empty, Hoverable, Icon, MouseState,
14 MouseStateHandle,
15 },
16 ui_components::{
17 components::{UiComponent, UiComponentStyles},
18 text::Span,
19 },
20};
21 
22/// Enum specifying relative alignment of the text and icon within
23/// a button. "First" is used instead of left/right to make this
24/// robust to RTL languages.
25#[derive(Clone, Copy, PartialEq, Eq)]
26pub enum TextAndIconAlignment {
27 /// Render the icon before the text.
28 IconFirst,
29 /// Render the text before the icon.
30 TextFirst,
31}
32 
33/// Configuration data for a button containing both a text and an icon.
34#[derive(Clone)]
35pub struct TextAndIcon {
36 alignment: TextAndIconAlignment,
37 /// The amount of space the `Flex` row should consume along the main axis.
38 flex_size: MainAxisSize,
39 /// The alignment strategy for rendering the `Flex`.
40 flex_spacing: MainAxisAlignment,
41 text: Cow<'static, str>,
42 icon: Icon,
43 /// Padding between the text and the icon.
44 padding: f32,
45 icon_size: Vector2F,
46}
47 
48impl TextAndIcon {
49 pub fn new(
50 alignment: TextAndIconAlignment,
51 text: impl Into<Cow<'static, str>>,
52 icon: Icon,
53 flex_size: MainAxisSize,
54 flex_spacing: MainAxisAlignment,
55 icon_size: Vector2F,
56 ) -> Self {
57 Self {
58 alignment,
59 flex_size,
60 flex_spacing,
61 text: text.into(),
62 icon,
63 padding: 0.,
64 icon_size,
65 }
66 }
67 
68 pub fn with_inner_padding(mut self, padding: f32) -> Self {
69 self.padding = padding;
70 self
71 }
72}
73 
74enum ButtonLabel {
75 None,
76 /// A start-aligned text label.
77 Text(String),
78 /// A center-aligned text label.
79 CenteredText(String),
80 Icon(Icon),
81 TextAndIcon(TextAndIcon),
82 Custom(Box<dyn Element>),
83}
84 
85pub struct Button {
86 label: ButtonLabel,
87 /// Should the button be clickable?
88 disabled: bool,
89 /// Was the button clicked and its state is active?
90 active: bool,
91 styles: UiComponentStyles,
92 /// Used when the button is hovered, if None - falls back to `styles`
93 hovered_styles: Option<UiComponentStyles>,
94 /// Used when the button is clicked, if None - falls back to `styles`
95 clicked_styles: Option<UiComponentStyles>,
96 /// Used when the button is disabled, if None - falls back to `styles`
97 disabled_styles: Option<UiComponentStyles>,
98 /// Used when the button is active, if None - falls back to `clicked_styles` when available,
99 /// or `styles` otherwise
100 active_styles: Option<UiComponentStyles>,
101 render_tooltip_fn: Option<Box<dyn FnOnce() -> Box<dyn Element>>>,
102 tooltip_position: ButtonTooltipPosition,
103 hover_state: MouseStateHandle,
104 cursor: Option<Cursor>,
105}
106 
107#[derive(Copy, Clone, Debug, PartialEq, Eq)]
108pub enum ButtonVariant {
109 Basic,
110 Secondary,
111 Accent,
112 Outlined,
113 Warn,
114 Error,
115 Text,
116 Link,
117}
118 
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
120pub enum ButtonTooltipPosition {
121 /// Position the tooltip above the button (center-aligned).
122 Above,
123 /// Position the tooltip below the button (center-aligned).
124 #[default]
125 Below,
126 /// Position the tooltip above the button (left-aligned).
127 AboveLeft,
128 /// Position the tooltip above the button (right-aligned).
129 AboveRight,
130 /// Position the tooltip below the button (left-aligned).
131 BelowLeft,
132 /// Position the tooltip below the button (right-aligned).
133 BelowRight,
134}
135 
136impl UiComponent for Button {
137 type ElementType = Hoverable;
138 fn build(self) -> Hoverable {
139 let disabled = self.disabled;
140 let cursor = self.cursor;
141 let mut hoverable = Hoverable::new(self.hover_state.clone(), |state| {
142 self.render_inner_button(state)
143 });
144 if let Some(cursor) = cursor {
145 hoverable = hoverable.with_cursor(cursor);
146 }
147 if disabled {
148 return hoverable.disable();
149 }
150 hoverable
151 }
152 
153 /// Overwrites _some_ styles passed in `style` parameter
154 fn with_style(self, styles: UiComponentStyles) -> Self {
155 Button {
156 styles: self.styles.merge(styles),
157 hovered_styles: Some(self.hovered_styles.unwrap_or(self.styles).merge(styles)),
158 clicked_styles: Some(self.clicked_styles.unwrap_or(self.styles).merge(styles)),
159 disabled_styles: Some(self.disabled_styles.unwrap_or(self.styles).merge(styles)),
160 active_styles: Some(self.active_styles.unwrap_or(self.styles).merge(styles)),
161 ..self
162 }
163 }
164}
165 
166impl Button {
167 pub fn new(
168 mouse_state: MouseStateHandle,
169 default_styles: UiComponentStyles,
170 hovered_styles: Option<UiComponentStyles>,
171 clicked_styles: Option<UiComponentStyles>,
172 disabled_styles: Option<UiComponentStyles>,
173 ) -> Self {
174 Button {
175 label: ButtonLabel::None,
176 disabled: false,
177 styles: default_styles,
178 hovered_styles,
179 clicked_styles,
180 disabled_styles,
181 active_styles: None,
182 active: false,
183 hover_state: mouse_state,
184 render_tooltip_fn: None,
185 tooltip_position: Default::default(),
186 cursor: Some(Cursor::PointingHand),
187 }
188 }
189 
190 pub fn disabled(mut self) -> Self {
191 self.disabled = true;
192 self
193 }
194 
195 pub fn active(mut self) -> Self {
196 self.active = true;
197 self
198 }
199 
200 pub fn with_text_label(mut self, label: String) -> Self {
201 self.label = ButtonLabel::Text(label);
202 self
203 }
204 
205 pub fn with_centered_text_label(mut self, label: String) -> Self {
206 self.label = ButtonLabel::CenteredText(label);
207 self
208 }
209 
210 pub fn with_icon_label(mut self, icon: Icon) -> Self {
211 self.label = ButtonLabel::Icon(icon);
212 self
213 }
214 
215 pub fn with_cursor(mut self, cursor: Option<Cursor>) -> Self {
216 self.cursor = cursor;
217 self
218 }
219 
220 pub fn with_active_styles(mut self, styles: UiComponentStyles) -> Self {
221 self.active_styles = Some(self.styles.merge(styles));
222 self
223 }
224 
225 pub fn with_hovered_styles(mut self, styles: UiComponentStyles) -> Self {
226 self.hovered_styles = Some(self.styles.merge(styles));
227 self
228 }
229 
230 pub fn hovered_styles(&self) -> &UiComponentStyles {
231 self.hovered_styles.as_ref().unwrap_or(&self.styles)
232 }
233 
234 pub fn with_disabled_styles(mut self, styles: UiComponentStyles) -> Self {
235 self.disabled_styles = Some(self.styles.merge(styles));
236 self
237 }
238 
239 pub fn with_clicked_styles(mut self, styles: UiComponentStyles) -> Self {
240 self.clicked_styles = Some(self.styles.merge(styles));
241 self
242 }
243 
244 /// Renders text followed by an icon within the Button.
245 pub fn with_text_and_icon_label(mut self, text_and_icon: TextAndIcon) -> Self {
246 self.label = ButtonLabel::TextAndIcon(text_and_icon);
247 self
248 }
249 
250 pub fn with_custom_label(mut self, label: Box<dyn Element>) -> Self {
251 self.label = ButtonLabel::Custom(label);
252 self
253 }
254 
255 pub fn with_tooltip<F>(mut self, render_tooltip_fn: F) -> Self
256 where
257 F: 'static + FnOnce() -> Box<dyn Element>,
258 {
259 self.render_tooltip_fn = Some(Box::new(render_tooltip_fn));
260 self
261 }
262 
263 /// Sets how the tooltip is positioned relative to the button itself. This only has an effect
264 /// if a tooltip is set with [`Self::with_tooltip`].
265 pub fn with_tooltip_position(mut self, position: ButtonTooltipPosition) -> Self {
266 self.tooltip_position = position;
267 self
268 }
269 
270 fn styles(&self, state: &MouseState) -> UiComponentStyles {
271 // disabled button ignores click/hover events
272 if self.disabled {
273 return self.disabled_styles.unwrap_or(self.styles);
274 }
275 
276 if self.active {
277 return self
278 .active_styles
279 .unwrap_or_else(|| self.clicked_styles.unwrap_or(self.styles));
280 }
281 
282 // For hover styles, we want to show the correct style based on
283 // where the mouse _currently_ is, rather than whether the element
284 // is considered hovered, because the latter takes into account delays.
285 if state.is_mouse_over_element() {
286 if state.is_clicked() {
287 return self.clicked_styles.unwrap_or(self.styles);
288 }
289 return self.hovered_styles.unwrap_or(self.styles);
290 }
291 self.styles
292 }
293 
294 fn render_inner_button(mut self, state: &MouseState) -> Box<dyn Element> {
295 let styles = self.styles(state);
296 // Text & font / Icon
297 let label = match self.label {
298 ButtonLabel::Text(text) => Span::new(text, styles).build().finish(),
299 ButtonLabel::CenteredText(text) => {
300 Align::new(Span::new(text, styles).build().finish()).finish()
301 }
302 ButtonLabel::Icon(icon) => {
303 if let Some(color) = styles.font_color {
304 icon.with_color(color).finish()
305 } else {
306 icon.finish()
307 }
308 }
309 ButtonLabel::TextAndIcon(text_and_icon) => {
310 let text = Shrinkable::new(
311 1.,
312 Container::new(Span::new(text_and_icon.text, styles).build().finish()).finish(),
313 )
314 .finish();
315 let icon = if let Some(color) = styles.font_color {
316 text_and_icon.icon.with_color(color).finish()
317 } else {
318 text_and_icon.icon.finish()
319 };
320 let icon = ConstrainedBox::new(icon)
321 .with_width(text_and_icon.icon_size.x())
322 .with_height(text_and_icon.icon_size.y())
323 .finish();
324 
325 let (first, second) = if text_and_icon.alignment == TextAndIconAlignment::TextFirst
326 {
327 (text, icon)
328 } else {
329 (icon, text)
330 };
331 
332 Flex::row()
333 .with_children([
334 first,
335 Container::new(second)
336 .with_padding_left(text_and_icon.padding)
337 .finish(),
338 ])
339 .with_cross_axis_alignment(CrossAxisAlignment::Center)
340 .with_main_axis_alignment(text_and_icon.flex_spacing)
341 .with_main_axis_size(text_and_icon.flex_size)
342 .finish()
343 }
344 ButtonLabel::Custom(element) => element,
345 ButtonLabel::None => Empty::new().finish(),
346 };
347 
348 let mut container = Container::new(label);
349 // Setting up the border
350 if let Some(corner) = styles.border_radius {
351 container = container.with_corner_radius(corner);
352 }
353 // TODO border width separate for top/left/right/bottom
354 let mut border = Border::all(styles.border_width.unwrap_or_default());
355 if let Some(border_color) = styles.border_color {
356 border = border.with_border_fill(border_color);
357 }
358 container = container.with_border(border);
359 
360 // Position-related settings
361 if let Some(padding) = styles.padding {
362 container = container
363 .with_padding_left(padding.left)
364 .with_padding_top(padding.top)
365 .with_padding_right(padding.right)
366 .with_padding_bottom(padding.bottom);
367 }
368 if let Some(margin) = styles.margin {
369 container = container
370 .with_margin_left(margin.left)
371 .with_margin_top(margin.top)
372 .with_margin_right(margin.right)
373 .with_margin_bottom(margin.bottom);
374 }
375 
376 if let Some(background) = styles.background {
377 container = container.with_background(background);
378 }
379 
380 let container = match (styles.height, styles.width) {
381 (None, None) => container.finish(),
382 (_, _) => {
383 let mut constrained_box = ConstrainedBox::new(container.finish());
384 if let Some(height) = styles.height {
385 constrained_box = constrained_box.with_height(height);
386 }
387 if let Some(width) = styles.width {
388 constrained_box = constrained_box.with_width(width);
389 }
390 constrained_box.finish()
391 }
392 };
393 
394 // The tooltip should only be shown if the element
395 // is considered hovered (accounting for delays).
396 if state.is_hovered() {
397 if let Some(render_tooltip_fn) = self.render_tooltip_fn.take() {
398 // Keep stack within this rather than using a stack for all cases to allow multiple stack overlays to work
399 let mut stack = Stack::new();
400 stack.add_child(container);
401 let tooltip = render_tooltip_fn();
402 let tooltip_offset = match self.tooltip_position {
403 ButtonTooltipPosition::Above => OffsetPositioning::offset_from_parent(
404 vec2f(0., -8.),
405 ParentOffsetBounds::WindowByPosition,
406 ParentAnchor::TopMiddle,
407 ChildAnchor::BottomMiddle,
408 ),
409 ButtonTooltipPosition::Below => OffsetPositioning::offset_from_parent(
410 vec2f(0., 8.),
411 ParentOffsetBounds::WindowByPosition,
412 ParentAnchor::BottomMiddle,
413 ChildAnchor::TopMiddle,
414 ),
415 ButtonTooltipPosition::AboveLeft => OffsetPositioning::offset_from_parent(
416 vec2f(0., -8.),
417 ParentOffsetBounds::WindowByPosition,
418 ParentAnchor::TopLeft,
419 ChildAnchor::BottomLeft,
420 ),
421 ButtonTooltipPosition::BelowLeft => OffsetPositioning::offset_from_parent(
422 vec2f(0., 8.),
423 ParentOffsetBounds::WindowByPosition,
424 ParentAnchor::BottomLeft,
425 ChildAnchor::TopLeft,
426 ),
427 ButtonTooltipPosition::AboveRight => OffsetPositioning::offset_from_parent(
428 vec2f(0., -8.),
429 ParentOffsetBounds::WindowByPosition,
430 ParentAnchor::TopRight,
431 ChildAnchor::BottomRight,
432 ),
433 ButtonTooltipPosition::BelowRight => OffsetPositioning::offset_from_parent(
434 vec2f(0., 8.),
435 ParentOffsetBounds::WindowByPosition,
436 ParentAnchor::BottomRight,
437 ChildAnchor::TopRight,
438 ),
439 };
440 stack.add_positioned_overlay_child(tooltip, tooltip_offset);
441 return stack.finish();
442 }
443 }
444 
445 container
446 }
447 
448 pub fn set_clicked_styles(mut self, styles: Option<UiComponentStyles>) -> Self {
449 self.clicked_styles = styles;
450 self
451 }
452}
453