use std::sync::Arc;
use rosace_core::types::{Rect, Size};
use rosace_render::Color;
use super::{Widget, LayoutCtx, PaintCtx};
use super::container::draw_rounded_rect_pub;
type Item = (String, Arc<dyn Fn() + Send + Sync>);
pub struct Menu {
items: Vec<Item>,
pub min_width: f32,
pub row_height: f32,
pub font_size: Option<f32>,
pub radius: f32,
background: Option<Color>,
color: Option<Color>,
}
impl Menu {
pub fn new() -> Self {
Self {
items: Vec::new(),
min_width: 180.0,
row_height: 34.0,
font_size: None,
radius: 14.0,
background: None,
color: None,
}
}
pub fn min_width(mut self, w: f32) -> Self { self.min_width = w; self }
pub fn row_height(mut self, h: f32) -> Self { self.row_height = h; self }
pub fn radius(mut self, r: f32) -> Self { self.radius = r; self }
pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
pub fn color(mut self, c: Color) -> Self { self.color = Some(c); self }
fn resolved_font_size(&self, theme: &rosace_theme::ThemeData) -> f32 {
self.font_size.unwrap_or(theme.typography.body_medium.size)
}
pub fn item(mut self, label: impl Into<String>, f: impl Fn() + Send + Sync + 'static) -> Self {
self.items.push((label.into(), Arc::new(f)));
self
}
}
impl Default for Menu {
fn default() -> Self { Self::new() }
}
const PAD_V: f32 = 6.0;
const PAD_H: f32 = 14.0;
impl Widget for Menu {
fn layout(&self, ctx: &LayoutCtx) -> Size {
let font_size = self.resolved_font_size(ctx.theme);
let widest = self.items.iter()
.map(|(label, _)| ctx.font.measure_text(label, font_size))
.fold(0.0_f32, f32::max);
let width = (widest + PAD_H * 2.0).max(self.min_width);
let height = self.items.len() as f32 * self.row_height + PAD_V * 2.0;
ctx.constraints.constrain(Size { width, height })
}
fn paint(&self, ctx: &mut PaintCtx) {
let (bg, fg, outline) = {
let t = &ctx.theme.colors;
let default_bg = {
let s = ctx.tc(t.surface);
Color { r: s.r, g: s.g, b: s.b, a: 216 }
};
(self.background.unwrap_or(default_bg),
self.color.unwrap_or_else(|| ctx.tc(t.on_surface)),
ctx.tc(t.outline))
};
let r = ctx.rect;
ctx.fill_shadow_rrect(r, self.radius, Color::rgba(0, 0, 0, 90), 10.0);
draw_rounded_rect_pub(ctx, r, bg, self.radius);
ctx.stroke_rrect(r, self.radius, Color { a: 120, ..outline }, 1.0);
let font_size = self.resolved_font_size(&ctx.theme);
let line_h = ctx.font.line_height(font_size);
let hi = ctx.tc(ctx.theme.colors.on_surface);
let with_alpha = |c: Color, a: f32| Color::rgba(c.r, c.g, c.b, (a.clamp(0.0, 1.0) * 255.0).round() as u8);
for (i, (label, cb)) in self.items.iter().enumerate() {
let row = Rect {
origin: rosace_core::types::Point {
x: r.origin.x,
y: r.origin.y + PAD_V + i as f32 * self.row_height,
},
size: Size { width: r.size.width, height: self.row_height },
};
let mut child = ctx.child(row);
let hov = child.hovered();
let prs = child.pressed();
if hov || prs {
let inset = Rect {
origin: rosace_core::types::Point { x: row.origin.x + 5.0, y: row.origin.y + 2.0 },
size: Size { width: row.size.width - 10.0, height: row.size.height - 4.0 },
};
child.fill_rrect(inset, 8.0, with_alpha(hi, if prs { 0.16 } else { 0.09 }));
}
let ty = row.origin.y + (self.row_height - line_h) / 2.0;
child.draw_text_at(label, rosace_core::types::Point { x: row.origin.x + PAD_H, y: ty }, fg, font_size);
child.semantics(super::Semantics::new(rosace_core::Role::MenuItem).label(label));
child.register_hit(Arc::clone(cb));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rosace_layout::Constraints;
#[test]
fn customization_builders_do_not_change_layout_size() {
let font = rosace_render::FontCache::embedded();
let theme = rosace_theme::built_in::dark_theme();
let ctx = LayoutCtx::new(Constraints::loose(400.0, 400.0), &font, &theme);
let base = Menu::new().item("Item one", || {});
let customized = Menu::new()
.background(Color::rgb(20, 20, 20))
.color(Color::rgb(255, 255, 255))
.radius(2.0)
.item("Item one", || {});
assert_eq!(base.layout(&ctx), customized.layout(&ctx));
}
}