1use std::sync::Arc;
2
3use rosace_core::types::{Point, Rect, Size};
4use rosace_layout::Constraints;
5use rosace_render::Color;
6use super::{Widget, LayoutCtx, PaintCtx, BoxedWidget, avail_h};
7use super::container::draw_rounded_rect_pub;
8
9fn with_alpha(c: Color, a: f32) -> Color {
10 Color::rgba(c.r, c.g, c.b, (a.clamp(0.0, 1.0) * 255.0).round() as u8)
11}
12
13pub struct NavItem {
15 pub label: String,
16 pub badge: Option<u32>,
17 pub active: bool,
18 pub leading: Option<BoxedWidget>,
19 pub height: f32,
20 pub font_size: Option<f32>,
24 on_press: Option<Arc<dyn Fn() + Send + Sync>>,
25}
26
27impl NavItem {
28 pub fn new(label: impl Into<String>) -> Self {
29 Self {
30 label: label.into(),
31 badge: None,
32 active: false,
33 leading: None,
34 height: 36.0,
35 font_size: None,
36 on_press: None,
37 }
38 }
39 pub fn active(mut self) -> Self { self.active = true; self }
40 pub fn active_if(mut self, c: bool) -> Self { self.active = c; self }
41 pub fn badge(mut self, n: u32) -> Self { self.badge = Some(n); self }
42 pub fn leading(mut self, w: impl Widget + 'static) -> Self { self.leading = Some(Box::new(w)); self }
43 pub fn height(mut self, h: f32) -> Self { self.height = h; self }
44
45 fn resolved_font_size(&self, theme: &rosace_theme::ThemeData) -> f32 {
46 self.font_size.unwrap_or(theme.typography.label_medium.size)
47 }
48 pub fn on_press(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
50 self.on_press = Some(Arc::new(f)); self
51 }
52}
53
54impl Widget for NavItem {
55 fn layout(&self, ctx: &LayoutCtx) -> Size {
56 let constraints = ctx.constraints;
57 Size { width: constraints.max_width_f32(), height: self.height }
58 }
59
60 fn paint(&self, ctx: &mut PaintCtx) {
61 let mut sem = super::Semantics::new(rosace_core::Role::Link).label(&self.label);
64 if let Some(n) = self.badge { sem = sem.value(n.to_string()); }
65 ctx.semantics(sem);
66 let r = ctx.rect;
67
68 match &self.on_press {
70 Some(cb) => ctx.register_hit(Arc::clone(cb)),
71 None => ctx.register_hit(Arc::new(|| {})),
72 }
73 let hovered = ctx.hovered();
74 let pressed = ctx.pressed();
75
76 let colors = ctx.theme.colors.clone();
77 let accent = ctx.tc(colors.primary);
78 let on_surf = ctx.tc(colors.on_surface);
79 let pill = Rect { origin: Point { x: r.origin.x + 6.0, y: r.origin.y + 2.0 },
80 size: Size { width: r.size.width - 12.0, height: r.size.height - 4.0 } };
81
82 if self.active {
84 draw_rounded_rect_pub(ctx, pill, with_alpha(accent, 0.16), 8.0);
85 ctx.fill_rect(Rect { origin: Point { x: r.origin.x, y: r.origin.y + 6.0 },
86 size: Size { width: 3.0, height: r.size.height - 12.0 } }, accent);
87 } else if hovered || pressed {
88 draw_rounded_rect_pub(ctx, pill, with_alpha(on_surf, if pressed { 0.12 } else { 0.07 }), 8.0);
89 }
90
91 let mut lx = r.origin.x + 14.0;
92
93 if let Some(lead) = &self.leading {
95 let ls = lead.layout(&ctx.layout_ctx(Constraints::loose(20.0, r.size.height)));
96 let ly = r.origin.y + (r.size.height - ls.height) / 2.0;
97 lead.paint(&mut ctx.child(Rect { origin: Point { x: lx, y: ly }, size: ls }));
98 lx += ls.width + 8.0;
99 }
100
101 let label_color = if self.active { on_surf }
103 else if hovered { super::lerp_color(with_alpha(on_surf, 0.6), on_surf, 0.5) }
104 else { with_alpha(on_surf, 0.6) };
105 let font_size = self.resolved_font_size(&ctx.theme);
106 let line_h = ctx.font.line_height(font_size);
107 let ty = r.origin.y + (r.size.height - line_h) / 2.0;
108 ctx.draw_text_at(&self.label, Point { x: lx, y: ty }, label_color, font_size);
109
110 if let Some(n) = self.badge {
111 let ns = n.to_string();
112 let bw = ns.len() as f32 * 7.0 + 8.0;
113 let bx = r.origin.x + r.size.width - bw - 10.0;
114 let by = r.origin.y + (r.size.height - 16.0) / 2.0;
115 let badge_col = if self.active { accent } else { with_alpha(on_surf, 0.18) };
116 draw_rounded_rect_pub(ctx, Rect { origin: Point { x: bx, y: by }, size: Size { width: bw, height: 16.0 } }, badge_col, 8.0);
117 let badge_text_color = if self.active { Color::rgb(252, 252, 255) } else { with_alpha(on_surf, 0.7) };
118 ctx.draw_text_at(&ns, Point { x: bx + 4.0, y: by + 3.0 }, badge_text_color, 8.5);
119 }
120 }
121}
122
123pub struct NavRail {
125 pub width: f32,
126 pub background: Color,
127 pub border_color: Color,
128 items: Vec<NavRailEntry>,
129}
130
131enum NavRailEntry {
132 Item(NavItem),
133 Section(String),
134 Separator,
135 Custom(Box<dyn Widget>),
136}
137
138impl NavRail {
139 pub fn new() -> Self {
140 Self {
141 width: 232.0,
142 background: Color::rgb(11, 12, 22),
143 border_color: Color::rgb(32, 35, 58),
144 items: Vec::new(),
145 }
146 }
147 pub fn width(mut self, w: f32) -> Self { self.width = w; self }
148 pub fn background(mut self, c: Color) -> Self { self.background = c; self }
149 pub fn item(mut self, i: NavItem) -> Self { self.items.push(NavRailEntry::Item(i)); self }
150 pub fn section(mut self, label: impl Into<String>) -> Self {
151 self.items.push(NavRailEntry::Section(label.into())); self
152 }
153 pub fn separator(mut self) -> Self { self.items.push(NavRailEntry::Separator); self }
154 pub fn widget(mut self, w: impl Widget + 'static) -> Self {
155 self.items.push(NavRailEntry::Custom(Box::new(w))); self
156 }
157}
158
159impl Default for NavRail {
160 fn default() -> Self { Self::new() }
161}
162
163impl Widget for NavRail {
164 fn layout(&self, ctx: &LayoutCtx) -> Size {
165 let constraints = ctx.constraints;
166 Size { width: self.width, height: avail_h(constraints) }
167 }
168
169 fn paint(&self, ctx: &mut PaintCtx) {
170 let r = ctx.rect;
171 ctx.fill_rect(r, self.background);
172 ctx.fill_rect(Rect {
173 origin: Point { x: r.origin.x + r.size.width - 1.0, y: r.origin.y },
174 size: Size { width: 1.0, height: r.size.height },
175 }, self.border_color);
176
177 let mut y = r.origin.y;
178 let _item_c = Constraints::tight(self.width, 32.0);
179
180 for entry in &self.items {
181 match entry {
182 NavRailEntry::Item(item) => {
183 let h = item.height;
184 item.paint(&mut ctx.child(Rect {
185 origin: Point { x: r.origin.x, y },
186 size: Size { width: self.width, height: h },
187 }));
188 y += h;
189 }
190 NavRailEntry::Section(label) => {
191 let section_rect = Rect { origin: Point { x: r.origin.x, y }, size: Size { width: self.width, height: 20.0 } };
192 ctx.child(section_rect).semantics(
193 super::Semantics::new(rosace_core::Role::Heading).label(label).heading_level(3),
194 );
195 ctx.draw_text_at(
196 label,
197 Point { x: r.origin.x + 14.0, y: y + 4.0 },
198 Color::rgb(80, 85, 118), 8.0,
199 );
200 y += 20.0;
201 }
202 NavRailEntry::Separator => {
203 ctx.fill_rect(Rect {
204 origin: Point { x: r.origin.x, y },
205 size: Size { width: self.width, height: 1.0 },
206 }, Color::rgb(24, 26, 44));
207 y += 10.0;
208 }
209 NavRailEntry::Custom(w) => {
210 let size = w.layout(&ctx.layout_ctx(Constraints::loose(self.width, r.size.height - (y - r.origin.y))));
211 w.paint(&mut ctx.child(Rect { origin: Point { x: r.origin.x, y }, size }));
212 y += size.height;
213 }
214 }
215 }
216 }
217}