teksilo_widgets/styles/
recipe_icon_button_style.rs1use teksilo_core::build_context::BuildContext;
18use teksilo_core::color_prop::ColorProp;
19use teksilo_core::signal::Signal;
20use teksilo_core::styles::{IconButtonSize, IconButtonStyle, IconButtonStyleConfig};
21use teksilo_core::widget_id::WidgetId;
22use teksilo_tokens::CornerRadius;
23use teksilo_tokens::{BorderRole, SurfaceRole};
24
25use crate::primitives::{Center, FixedSize, RectWidget, ZStack};
26
27pub const ICON_BUTTON_SIZE_COMPACT: f32 = 24.0;
33pub const ICON_BUTTON_SIZE_DEFAULT: f32 = 24.0;
34pub const ICON_BUTTON_SIZE_TOOLBAR: f32 = 30.0;
35pub const ICON_BUTTON_SIZE_LARGE: f32 = 40.0;
36pub const ICON_BUTTON_SIZE_HERO: f32 = 50.0;
37pub const ICON_BUTTON_ICON_SIZE: f32 = 16.0;
38pub const ICON_BUTTON_ICON_SIZE_TOOLBAR: f32 = 18.0;
39pub const ICON_BUTTON_ICON_SIZE_LARGE: f32 = 24.0;
40pub const ICON_BUTTON_ICON_SIZE_HERO: f32 = 32.0;
41pub const ICON_BUTTON_CORNER_RADIUS: f32 = 8.0;
42
43#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct IconButtonRecipe {
48 pub size_compact: f32,
49 pub size_default: f32,
50 pub size_toolbar: f32,
51 pub size_large: f32,
52 pub size_hero: f32,
53 pub icon_size: f32,
54 pub icon_size_toolbar: f32,
55 pub icon_size_large: f32,
56 pub icon_size_hero: f32,
57 pub corner_radius: f32,
58}
59
60impl Default for IconButtonRecipe {
61 fn default() -> Self {
62 Self {
63 size_compact: ICON_BUTTON_SIZE_COMPACT,
64 size_default: ICON_BUTTON_SIZE_DEFAULT,
65 size_toolbar: ICON_BUTTON_SIZE_TOOLBAR,
66 size_large: ICON_BUTTON_SIZE_LARGE,
67 size_hero: ICON_BUTTON_SIZE_HERO,
68 icon_size: ICON_BUTTON_ICON_SIZE,
69 icon_size_toolbar: ICON_BUTTON_ICON_SIZE_TOOLBAR,
70 icon_size_large: ICON_BUTTON_ICON_SIZE_LARGE,
71 icon_size_hero: ICON_BUTTON_ICON_SIZE_HERO,
72 corner_radius: ICON_BUTTON_CORNER_RADIUS,
73 }
74 }
75}
76
77#[derive(Debug, Default, Clone, Copy)]
81pub struct RecipeIconButtonStyle {
82 pub recipe: IconButtonRecipe,
83}
84
85impl RecipeIconButtonStyle {
86 pub fn new(recipe: IconButtonRecipe) -> Self {
87 Self { recipe }
88 }
89}
90
91impl IconButtonStyle for RecipeIconButtonStyle {
92 fn make_body(&self, cfg: &IconButtonStyleConfig, ctx: &mut BuildContext) -> WidgetId {
93 let focus_ring_width = ctx.theme().shape.focus_ring_width;
94 let corner_radius = self.recipe.corner_radius;
95 let button_dim = resolve_size(cfg.size, &self.recipe);
96
97 let bg_role: ColorProp = match cfg.is_on.clone() {
101 Some(on) => {
102 bistate_bg_role(&cfg.is_pressed, &cfg.is_hovered, &cfg.is_disabled, &on).into()
103 }
104 None => plain_bg_role(&cfg.is_pressed, &cfg.is_hovered, &cfg.is_disabled).into(),
105 };
106
107 let border_role: ColorProp = cfg
111 .is_focused
112 .map(|focused| {
113 if *focused {
114 BorderRole::Focused
115 } else {
116 BorderRole::Transparent
117 }
118 })
119 .into();
120 let border_width = cfg
121 .is_focused
122 .map(move |focused| if *focused { focus_ring_width } else { 0.0 });
123
124 let bg_id = ctx.add(
125 RectWidget::new()
126 .background(bg_role)
127 .border_color(border_role)
128 .border_width(border_width)
129 .corner_radius(CornerRadius::uniform(corner_radius)),
130 );
131
132 let centered_id = ctx.add(Center::new().child_id(cfg.icon));
133 let zstack_id = ctx.add(ZStack::new().add_child(bg_id).add_child(centered_id));
134 ctx.add(
135 FixedSize::new()
136 .width(button_dim)
137 .height(button_dim)
138 .child_id(zstack_id),
139 )
140 }
141}
142
143fn plain_bg_role(
144 is_pressed: &Signal<bool>,
145 is_hovered: &Signal<bool>,
146 is_disabled: &Signal<bool>,
147) -> Signal<SurfaceRole> {
148 is_pressed
149 .zip3(is_hovered, is_disabled)
150 .map(|(pressed, hovered, disabled)| {
151 if *disabled {
156 SurfaceRole::Transparent
157 } else if *pressed {
158 SurfaceRole::Pressed
159 } else if *hovered {
160 SurfaceRole::Hover
161 } else {
162 SurfaceRole::Transparent
163 }
164 })
165}
166
167fn bistate_bg_role(
168 is_pressed: &Signal<bool>,
169 is_hovered: &Signal<bool>,
170 is_disabled: &Signal<bool>,
171 is_on: &Signal<bool>,
172) -> Signal<SurfaceRole> {
173 let combined = is_pressed.zip3(is_hovered, is_disabled);
174 combined
175 .zip(is_on)
176 .map(|((pressed, hovered, disabled), on)| {
177 if *disabled {
178 SurfaceRole::Transparent
179 } else if *on {
180 if *pressed {
181 SurfaceRole::Pressed
182 } else {
183 SurfaceRole::Selected
184 }
185 } else if *pressed {
186 SurfaceRole::Pressed
187 } else if *hovered {
188 SurfaceRole::Hover
189 } else {
190 SurfaceRole::Transparent
191 }
192 })
193}
194
195fn resolve_size(size: IconButtonSize, recipe: &IconButtonRecipe) -> f32 {
196 match size {
197 IconButtonSize::Compact => recipe.size_compact,
198 IconButtonSize::Default => recipe.size_default,
199 IconButtonSize::Toolbar => recipe.size_toolbar,
200 IconButtonSize::Large => recipe.size_large,
201 IconButtonSize::Hero => recipe.size_hero,
202 }
203}