teksilo_widgets/styles/
recipe_menu_item_style.rs1use teksilo_core::build_context::BuildContext;
20use teksilo_core::signal::Signal;
21use teksilo_core::styles::{MenuItemStyle, MenuItemStyleConfig};
22use teksilo_core::widget_id::WidgetId;
23use teksilo_tokens::{CornerRadius, SurfaceRole};
24
25use crate::primitives::{FixedSize, HStack, MinSize, Padding, RectWidget, Spacer, ZStack};
26
27pub const MENU_ITEM_HEIGHT: f32 = 24.0;
34pub const MENU_ITEM_PADDING_HORIZONTAL: f32 = 12.0;
36pub const MENU_ITEM_PADDING_LEADING: f32 = 6.0;
38pub const MENU_ICON_COLUMN_WIDTH: f32 = 16.0;
39pub const MENU_ICON_LABEL_GAP: f32 = 6.0;
40pub const MENU_SHORTCUT_LEFT_GAP: f32 = 24.0;
41pub const MENU_SEPARATOR_HEIGHT: f32 = 9.0;
42pub const MENU_ITEM_CORNER_RADIUS: f32 = 8.0;
44
45#[derive(Debug, Clone, Copy, PartialEq)]
51pub struct MenuItemRecipe {
52 pub item_height: f32,
53 pub padding_horizontal: f32,
54 pub padding_leading: f32,
55 pub icon_column_width: f32,
56 pub icon_label_gap: f32,
57 pub shortcut_left_gap: f32,
58 pub separator_height: f32,
59 pub item_corner_radius: f32,
60}
61
62impl Default for MenuItemRecipe {
63 fn default() -> Self {
64 Self {
65 item_height: MENU_ITEM_HEIGHT,
66 padding_horizontal: MENU_ITEM_PADDING_HORIZONTAL,
67 padding_leading: MENU_ITEM_PADDING_LEADING,
68 icon_column_width: MENU_ICON_COLUMN_WIDTH,
69 icon_label_gap: MENU_ICON_LABEL_GAP,
70 shortcut_left_gap: MENU_SHORTCUT_LEFT_GAP,
71 separator_height: MENU_SEPARATOR_HEIGHT,
72 item_corner_radius: MENU_ITEM_CORNER_RADIUS,
73 }
74 }
75}
76
77#[derive(Debug, Default, Clone, Copy)]
80pub struct RecipeMenuItemStyle {
81 pub recipe: MenuItemRecipe,
82}
83
84impl RecipeMenuItemStyle {
85 pub fn new(recipe: MenuItemRecipe) -> Self {
86 Self { recipe }
87 }
88}
89
90impl MenuItemStyle for RecipeMenuItemStyle {
91 fn make_body(&self, cfg: &MenuItemStyleConfig, ctx: &mut BuildContext) -> WidgetId {
92 let recipe = self.recipe;
93
94 let mut row = HStack::new().spacing(0.0);
99
100 if let Some(leading) = cfg.leading {
101 row = row.add_child(leading);
102 let gap_spacer = ctx.add(Spacer::new());
106 let gap = ctx.add(
107 FixedSize::new()
108 .width(recipe.icon_label_gap)
109 .height(1.0_f32)
110 .child_id(gap_spacer),
111 );
112 row = row.add_child(gap);
113 }
114
115 row = row.add_child(cfg.label);
116 row = row.child(MinSize::width(recipe.shortcut_left_gap).child(Spacer::new()));
119
120 if let Some(trailing) = cfg.trailing {
121 row = row.add_child(trailing);
122 }
123
124 let row_id = ctx.add(row);
125
126 let body = &ctx.theme().typography.body;
132 let body_line = body.size * body.line_height;
133 let pad_v = ((recipe.item_height - body_line) * 0.5).max(0.0);
134 let padding =
135 ctx.add(Padding::new(pad_v, 0.0, pad_v, recipe.padding_leading).child_id(row_id));
136
137 let bg_role = bg_signal(
141 &cfg.is_pressed,
142 &cfg.is_hovered,
143 &cfg.is_highlighted,
144 &cfg.is_disabled,
145 );
146 let bg = ctx.add(
147 RectWidget::new()
148 .background(bg_role)
149 .corner_radius(CornerRadius::uniform(recipe.item_corner_radius)),
150 );
151
152 ctx.add(ZStack::new().add_child(bg).add_child(padding))
153 }
154}
155
156fn bg_signal(
157 is_pressed: &Signal<bool>,
158 is_hovered: &Signal<bool>,
159 is_highlighted: &Signal<bool>,
160 is_disabled: &Signal<bool>,
161) -> Signal<SurfaceRole> {
162 let combined = is_pressed.zip3(is_hovered, is_highlighted);
163 combined
164 .zip(is_disabled)
165 .map(|((pressed, hovered, highlighted), disabled)| {
166 if *disabled {
167 SurfaceRole::Transparent
168 } else if *pressed {
169 SurfaceRole::Pressed
170 } else if *hovered || *highlighted {
171 SurfaceRole::AccentSubtle
172 } else {
173 SurfaceRole::Transparent
174 }
175 })
176}