1use rosace_core::types::{Point, Rect, Size};
2use rosace_layout::Constraints;
3use rosace_render::{Color, DrawCommand};
4use rosace_shader::ShaderMaterial;
5use rosace_theme::TitleAlign;
6use super::{Widget, LayoutCtx, PaintCtx, BoxedWidget, avail_w};
7use super::material::{resolve_material, AppBarMaterial};
8
9pub struct AppBar {
19 pub title: String,
20 pub title_size: f32,
21 pub background: Color,
22 pub foreground: Color,
23 pub border_color: Color,
24 height: Option<f32>,
26 pub leading: Option<BoxedWidget>,
27 pub actions: Vec<BoxedWidget>,
28 show_traffic_lights: Option<bool>,
30 material: Option<ShaderMaterial>,
31}
32
33impl AppBar {
34 pub fn new(title: impl Into<String>) -> Self {
35 Self {
36 title: title.into(),
37 title_size: 13.0,
38 background: Color::rgba(0, 0, 0, 0), foreground: Color::rgba(0, 0, 0, 0), border_color: Color::rgba(0, 0, 0, 0), height: None,
42 leading: None,
43 actions: Vec::new(),
44 show_traffic_lights: None,
45 material: None,
46 }
47 }
48
49 pub fn background(mut self, c: Color) -> Self { self.background = c; self }
50 pub fn foreground(mut self, c: Color) -> Self { self.foreground = c; self }
51 pub fn height(mut self, h: f32) -> Self { self.height = Some(h); self }
53 pub fn leading(mut self, w: impl Widget + 'static) -> Self { self.leading = Some(Box::new(w)); self }
54 pub fn action(mut self, w: impl Widget + 'static) -> Self { self.actions.push(Box::new(w)); self }
55 pub fn no_traffic_lights(mut self) -> Self { self.show_traffic_lights = Some(false); self }
57 pub fn traffic_lights(mut self) -> Self { self.show_traffic_lights = Some(true); self }
61 pub fn title_size(mut self, s: f32) -> Self { self.title_size = s; self }
62 pub fn material(mut self, m: ShaderMaterial) -> Self { self.material = Some(m); self }
65
66 fn effective_height(&self, theme: &rosace_theme::ThemeData) -> f32 {
67 self.height.unwrap_or(theme.app_bar.height)
68 }
69}
70
71impl Widget for AppBar {
72 fn layout(&self, ctx: &LayoutCtx) -> Size {
73 let constraints = ctx.constraints;
74 Size { width: avail_w(constraints), height: self.effective_height(ctx.theme) }
75 }
76
77 fn paint(&self, ctx: &mut PaintCtx) {
78 ctx.semantics(super::Semantics::new(rosace_core::Role::Heading).label(&self.title).heading_level(1));
81 let style = ctx.theme.app_bar;
82 let t = &ctx.theme.colors;
83 let bg = if self.background.a == 0 { ctx.tc(t.surface) } else { self.background };
84 let fg = if self.foreground.a == 0 { ctx.tc(t.on_surface) } else { self.foreground };
85 let border = if self.border_color.a == 0 { ctx.tc(t.outline) } else { self.border_color };
86 let show_traffic_lights = self.show_traffic_lights.unwrap_or(style.show_traffic_lights);
87
88 let r = ctx.rect;
89 let material = resolve_material::<AppBarMaterial>(&ctx.theme, self.material.as_ref());
94 match &material {
95 Some(m) => {
96 if let Some(fallback) = m.fallback {
97 ctx.fill_rect(r, fallback);
98 }
99 ctx.shader_fill(r, m.pipeline, m.uniforms.clone());
100 }
101 None => ctx.fill_rect(r, bg),
102 }
103
104 if style.elevation > 0.0 {
118 let shadow = ctx.tc(ctx.theme.colors.shadow);
119 ctx.record(DrawCommand::PushClip { rect: r });
120 let blur = (3.0 * style.elevation).clamp(2.0, 14.0);
121 let alpha = (70.0 * style.elevation).clamp(0.0, 130.0) as u8;
122 ctx.fill_shadow_rrect(
123 Rect {
124 origin: Point { x: r.origin.x, y: r.origin.y + r.size.height },
125 size: Size { width: r.size.width, height: 1.0 },
126 },
127 0.0,
128 Color::rgba(shadow.r, shadow.g, shadow.b, alpha),
129 blur,
130 );
131 ctx.record(DrawCommand::PopClip);
132 } else {
133 ctx.fill_rect(Rect {
137 origin: Point { x: r.origin.x, y: r.origin.y + r.size.height - 1.0 },
138 size: Size { width: r.size.width, height: 1.0 },
139 }, border);
140 }
141
142 let cy = r.origin.y + r.size.height / 2.0;
143 let mut lx = r.origin.x + 16.0;
144
145 if show_traffic_lights {
147 for (i, color) in [
148 Color::rgb(235, 85, 75),
149 Color::rgb(245, 185, 55),
150 Color::rgb(75, 200, 85),
151 ].iter().enumerate() {
152 ctx.fill_circle(Point { x: lx + i as f32 * 20.0, y: cy }, 7.0, *color);
153 }
154 lx += 72.0;
155 }
156
157 let height = self.effective_height(&ctx.theme);
160 if let Some(lead) = &self.leading {
161 let ls = lead.layout(&ctx.layout_ctx(Constraints::loose(160.0, height)));
162 let ly = r.origin.y + (r.size.height - ls.height) / 2.0;
163 lead.paint(&mut ctx.child(Rect { origin: Point { x: lx, y: ly }, size: ls }));
164 lx += ls.width + 12.0;
165 }
166
167 let mut ax = r.origin.x + r.size.width - 12.0;
170 for action in self.actions.iter().rev() {
171 let as_ = action.layout(&ctx.layout_ctx(Constraints::loose(160.0, height)));
172 ax -= as_.width + 6.0;
173 let ay = r.origin.y + (r.size.height - as_.height) / 2.0;
174 action.paint(&mut ctx.child(Rect { origin: Point { x: ax, y: ay }, size: as_ }));
175 }
176
177 let region_l = lx;
187 let region_r = (ax - 8.0).max(region_l);
188 let region_w = (region_r - region_l).max(0.0);
189 if region_w > 4.0 {
190 let title_w = ctx.font.measure_text(&self.title, self.title_size);
191 let line_h = ctx.font.line_height(self.title_size);
192 let title_y = r.origin.y + (r.size.height - line_h) / 2.0;
193 let title_x = match style.title_align {
194 TitleAlign::Leading => {
195 if title_w <= region_w {
196 region_l + (region_w - title_w) / 2.0
197 } else {
198 region_l
199 }
200 }
201 TitleAlign::Center => {
202 let full_center = r.origin.x + (r.size.width - title_w) / 2.0;
203 full_center.clamp(region_l, (region_r - title_w).max(region_l))
204 }
205 };
206 let clip = Rect {
207 origin: Point { x: region_l, y: r.origin.y },
208 size: Size { width: region_w, height: r.size.height },
209 };
210 ctx.record(DrawCommand::PushClip { rect: clip });
211 ctx.draw_text_at(&self.title, Point { x: title_x, y: title_y }, fg, self.title_size);
212 ctx.record(DrawCommand::PopClip);
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn instance_material_paints_a_shader_fill() {
223 let font = rosace_render::FontCache::embedded();
224 let theme = rosace_theme::built_in::dark_theme();
225 let mut recorder = rosace_render::PictureRecorder::new();
226 let tree = std::rc::Rc::new(std::cell::RefCell::new(super::super::render_tree::RenderTree::new()));
227 let rect = Rect {
228 origin: Point { x: 0.0, y: 0.0 },
229 size: Size { width: 400.0, height: 44.0 },
230 };
231 let mut ctx = PaintCtx::root(&mut recorder, rect, &font, theme, tree);
232 let m = ShaderMaterial::new(rosace_shader::PipelineId::user(0x4004), vec![0u8; 16]);
233 AppBar::new("t").material(m).paint(&mut ctx);
234 let picture = recorder.finish();
235 assert!(picture.commands.iter().any(|c| matches!(c, rosace_render::DrawCommand::ShaderFill { .. })));
236 }
237}