tessera_components/menus/
appearance.rs1use tessera_ui::{
8 Color, Dp, FocusScopeNode, Modifier, RenderSlot,
9 accesskit::Role,
10 current_frame_nanos,
11 layout::layout,
12 modifier::{FocusModifierExt as _, ModifierCapabilityExt as _},
13 provide_context, receive_frame_nanos, remember, tessera, use_context,
14};
15
16use crate::{
17 alignment::CrossAxisAlignment,
18 column::column,
19 modifier::{ModifierExt as _, Padding},
20 shape_def::{RoundedCorner, Shape},
21 surface::{SurfaceStyle, surface},
22 theme::{MaterialColorScheme, MaterialTheme},
23};
24
25use super::motion::{MenuShapeHover, MenuShapeMotion, advance};
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub enum MenuColorStyle {
30 #[default]
32 Standard,
33 Vibrant,
35}
36
37impl MenuColorStyle {
38 pub(super) fn container(self, scheme: &MaterialColorScheme) -> Color {
39 match self {
40 Self::Standard => scheme.surface_container_low,
41 Self::Vibrant => scheme.tertiary_container,
42 }
43 }
44
45 pub(super) fn selected_container(self, scheme: &MaterialColorScheme) -> Color {
46 match self {
47 Self::Standard => scheme.tertiary_container,
48 Self::Vibrant => scheme.tertiary,
49 }
50 }
51
52 pub(super) fn content(self, scheme: &MaterialColorScheme, selected: bool) -> Color {
53 match (self, selected) {
54 (Self::Standard, false) => scheme.on_surface,
55 (Self::Standard, true) | (Self::Vibrant, false) => scheme.on_tertiary_container,
56 (Self::Vibrant, true) => scheme.on_tertiary,
57 }
58 }
59
60 pub(super) fn supporting(self, scheme: &MaterialColorScheme, selected: bool) -> Color {
61 if self == Self::Standard && !selected {
62 scheme.on_surface_variant
63 } else {
64 self.content(scheme, selected)
65 }
66 }
67}
68
69#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
71pub enum MenuPosition {
72 #[default]
74 Standalone,
75 First,
77 Middle,
79 Last,
81}
82
83pub struct MenuDefaults;
85
86impl MenuDefaults {
87 pub const GROUP_SPACING: Dp = Dp(2.0);
89 pub const ITEM_HEIGHT: Dp = Dp(44.0);
91
92 pub fn group_shape(position: MenuPosition) -> Shape {
94 let (top, bottom) = match position {
95 MenuPosition::Standalone => (16.0, 16.0),
96 MenuPosition::First => (16.0, 8.0),
97 MenuPosition::Middle => (8.0, 8.0),
98 MenuPosition::Last => (8.0, 16.0),
99 };
100 corners(top, bottom)
101 }
102
103 pub fn item_shape(position: MenuPosition, selected: bool) -> Shape {
105 let (top, bottom) = if selected {
106 (12.0, 12.0)
107 } else {
108 match position {
109 MenuPosition::First => (12.0, 4.0),
110 MenuPosition::Last => (4.0, 12.0),
111 MenuPosition::Standalone | MenuPosition::Middle => (4.0, 4.0),
112 }
113 };
114 corners(top, bottom)
115 }
116}
117
118fn corners(top: f64, bottom: f64) -> Shape {
119 Shape::RoundedRectangle {
120 top_left: RoundedCorner::manual(Dp(top), 2.0),
121 top_right: RoundedCorner::manual(Dp(top), 2.0),
122 bottom_right: RoundedCorner::manual(Dp(bottom), 2.0),
123 bottom_left: RoundedCorner::manual(Dp(bottom), 2.0),
124 }
125}
126
127#[tessera]
178pub fn menu_group(
179 menu: Option<super::MenuScope>,
180 position: Option<MenuPosition>,
181 color_style: Option<MenuColorStyle>,
182 modifier: Option<Modifier>,
183 shape: Option<Shape>,
184 container_color: Option<Color>,
185 #[prop(into)] accessibility_label: Option<String>,
186 children: Option<RenderSlot>,
187) {
188 let inherited_style = use_context::<MenuColorStyle>().map(|style| style.get());
189 let color_style = color_style.or(inherited_style).unwrap_or_default();
190 let position = position.unwrap_or_default();
191 let scheme = use_context::<MaterialTheme>()
192 .expect("MaterialTheme must be provided")
193 .get()
194 .color_scheme;
195 let children = children.unwrap_or_else(RenderSlot::empty);
196 let modifier = modifier.unwrap_or_default();
197 let focus_scope = remember(FocusScopeNode::new).get();
198 let frame = current_frame_nanos();
199 let motion = remember(|| MenuShapeMotion::new(position, frame));
200 let focused = focus_scope.has_focus();
201 if shape.is_none() && motion.with(|motion| motion.needs_update(position, focused)) {
202 motion.with_mut(|motion| motion.update(position, focused, frame));
203 }
204 if shape.is_none() && !motion.with(MenuShapeMotion::is_idle) {
205 receive_frame_nanos(move |frame| advance(motion, frame));
206 }
207 let mut modifier = modifier.focus_scope_with(focus_scope);
208 if shape.is_none() {
209 modifier = modifier.push_pointer_preview_input(MenuShapeHover {
210 motion,
211 host: menu.map(|menu| menu.host),
212 controller: menu.map(|menu| menu.controller),
213 });
214 }
215 layout()
216 .modifier(Modifier::new().padding(Padding::top(
217 if matches!(position, MenuPosition::Middle | MenuPosition::Last) {
218 MenuDefaults::GROUP_SPACING
219 } else {
220 Dp::ZERO
221 },
222 )))
223 .child(move || {
224 surface()
225 .modifier(modifier.clone().fill_max_width())
226 .shape(shape.unwrap_or_else(|| motion.with(MenuShapeMotion::shape)))
227 .style(SurfaceStyle::Filled {
228 color: container_color.unwrap_or_else(|| color_style.container(&scheme)),
229 })
230 .accessibility_role(Role::Group)
231 .accessibility_label_optional(accessibility_label.clone())
232 .elevation(Dp(3.0))
233 .child(move || {
234 provide_context(
235 || color_style,
236 move || {
237 let inherited = use_context::<MenuColorStyle>().expect("menu colors");
238 if inherited.get() != color_style {
239 inherited.set(color_style);
240 }
241 column()
242 .modifier(
243 Modifier::new()
244 .fill_max_width()
245 .clip_shape(
246 shape.unwrap_or_else(|| {
247 motion.with(MenuShapeMotion::shape)
248 }),
249 )
250 .padding_symmetric(Dp::ZERO, Dp(2.0)),
251 )
252 .cross_axis_alignment(CrossAxisAlignment::Start)
253 .children_shared(children);
254 },
255 );
256 });
257 });
258}