StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use crate::{ |
| 2 | elements::{Border, Container, Element, Hoverable, MouseState, MouseStateHandle, Text}, |
| 3 | fonts::Properties, |
| 4 | platform::Cursor, |
| 5 | ui_components::components::{UiComponent, UiComponentStyles}, |
| 6 | EventContext, |
| 7 | }; |
| 8 | |
| 9 | pub type OnClickFn = Box<dyn Fn(&mut EventContext)>; |
| 10 | |
| 11 | pub struct Link { |
| 12 | text: String, // TODO figure out how it can be ui element (or icon?) |
| 13 | /// A URL that should be opened in the user's default web browser when clicked. |
| 14 | url: Option<String>, |
| 15 | /// A callback that should be fired when clicked. |
| 16 | /// Commonly dispatches an action from the calling view. |
| 17 | callback: Option<OnClickFn>, |
| 18 | styles: LinkStyles, |
| 19 | hover_state: MouseStateHandle, |
| 20 | } |
| 21 | |
| 22 | #[derive(Copy, Clone)] |
| 23 | pub struct LinkStyles { |
| 24 | pub base: UiComponentStyles, |
| 25 | pub hovered: Option<UiComponentStyles>, |
| 26 | pub clicked: Option<UiComponentStyles>, |
| 27 | pub soft_wrap: bool, |
| 28 | } |
| 29 | |
| 30 | impl LinkStyles { |
| 31 | fn merge(&self, style: UiComponentStyles) -> Self { |
| 32 | Self { |
| 33 | base: self.base.merge(style), |
| 34 | hovered: Some(self.hovered.unwrap_or(self.base).merge(style)), |
| 35 | clicked: Some(self.clicked.unwrap_or(self.base).merge(style)), |
| 36 | soft_wrap: self.soft_wrap, |
| 37 | } |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | impl UiComponent for Link { |
| 42 | type ElementType = Container; |
| 43 | fn build(self) -> Container { |
| 44 | let url = self.url.clone(); |
| 45 | Container::new( |
| 46 | Hoverable::new(self.hover_state.clone(), |state| { |
| 47 | let styles = self.styles(state); |
| 48 | let mut text = Text::new( |
| 49 | self.text.clone(), |
| 50 | styles.font_family_id.unwrap(), |
| 51 | styles.font_size.unwrap_or(14.), |
| 52 | ) |
| 53 | .soft_wrap(self.styles.soft_wrap); |
| 54 | |
| 55 | if let Some(font_color) = styles.font_color { |
| 56 | text = text.with_color(font_color); |
| 57 | } |
| 58 | |
| 59 | if let Some(weight) = styles.font_weight { |
| 60 | text = text.with_style(Properties::default().weight(weight)); |
| 61 | } |
| 62 | |
| 63 | match (styles.border_width, styles.border_color) { |
| 64 | (Some(border_width), Some(border_color)) => Container::new(text.finish()) |
| 65 | .with_border(Border::bottom(border_width).with_border_fill(border_color)) |
| 66 | // Pull down the element by 1px so that the 1px border doesn't affect the |
| 67 | // vertical positioning of the element. Without this, the text won't be |
| 68 | // vertically aligned with neighboring `Text` elements. |
| 69 | .with_margin_bottom(-border_width) |
| 70 | .finish(), |
| 71 | (_, _) => text.finish(), |
| 72 | } |
| 73 | }) |
| 74 | .on_click(move |ctx, app, _| { |
| 75 | if let Some(url) = &url { |
| 76 | app.open_url(url); |
| 77 | } |
| 78 | |
| 79 | if let Some(callback) = &self.callback { |
| 80 | callback(ctx); |
| 81 | } |
| 82 | }) |
| 83 | .with_cursor(Cursor::PointingHand) |
| 84 | .finish(), |
| 85 | ) |
| 86 | } |
| 87 | |
| 88 | /// Overwrites _some_ styles passed in `style` parameter |
| 89 | fn with_style(self, styles: UiComponentStyles) -> Self { |
| 90 | Link { |
| 91 | text: self.text.clone(), |
| 92 | url: self.url, |
| 93 | callback: self.callback, |
| 94 | styles: self.styles.merge(styles), |
| 95 | hover_state: self.hover_state, |
| 96 | } |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | impl Link { |
| 101 | pub fn new( |
| 102 | text: String, |
| 103 | url: Option<String>, |
| 104 | callback: Option<OnClickFn>, |
| 105 | mouse_state: MouseStateHandle, |
| 106 | styles: LinkStyles, |
| 107 | ) -> Self { |
| 108 | Link { |
| 109 | text, |
| 110 | url, |
| 111 | callback, |
| 112 | styles, |
| 113 | hover_state: mouse_state, |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | fn styles(&self, state: &MouseState) -> UiComponentStyles { |
| 118 | if state.is_hovered() { |
| 119 | if state.is_clicked() { |
| 120 | return self.styles.clicked.unwrap_or(self.styles.base); |
| 121 | } |
| 122 | return self.styles.hovered.unwrap_or(self.styles.base); |
| 123 | } |
| 124 | self.styles.base |
| 125 | } |
| 126 | |
| 127 | pub fn with_hovered_style(mut self, hover_style: UiComponentStyles) -> Self { |
| 128 | if let Some(style) = &mut self.styles.hovered { |
| 129 | *style = style.merge(hover_style); |
| 130 | } |
| 131 | self |
| 132 | } |
| 133 | |
| 134 | pub fn with_clicked_style(mut self, hover_style: UiComponentStyles) -> Self { |
| 135 | if let Some(style) = &mut self.styles.clicked { |
| 136 | *style = style.merge(hover_style); |
| 137 | } |
| 138 | self |
| 139 | } |
| 140 | |
| 141 | pub fn soft_wrap(mut self, soft_wrap: bool) -> Self { |
| 142 | self.styles.soft_wrap = soft_wrap; |
| 143 | self |
| 144 | } |
| 145 | } |
| 146 |