rosace_widgets/tree/
accordion.rs1use rosace_core::types::{Point, Rect, Size};
2use rosace_layout::Constraints;
3use rosace_render::Color;
4use rosace_state::Atom;
5
6use super::container::draw_rounded_rect_pub;
7use super::{BoxedWidget, LayoutCtx, PaintCtx, Widget};
8
9pub struct Accordion {
19 title: String,
20 expanded: Atom<bool>,
21 body: BoxedWidget,
22 background: Option<Color>,
23 border: Option<(Color, f32)>,
24 radius: f32,
25 elevation: f32,
27 title_size: f32,
28}
29
30impl Accordion {
31 pub fn new(title: impl Into<String>, expanded: Atom<bool>, body: impl Widget + 'static) -> Self {
32 Self {
33 title: title.into(),
34 expanded,
35 body: Box::new(body),
36 background: None,
37 border: None,
38 radius: 10.0,
39 elevation: 0.0,
40 title_size: 15.0,
41 }
42 }
43 pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
45 pub fn border(mut self, c: Color, width: f32) -> Self { self.border = Some((c, width)); self }
47 pub fn radius(mut self, r: f32) -> Self { self.radius = r; self }
48 pub fn elevation(mut self, e: f32) -> Self { self.elevation = e; self }
49 pub fn title_size(mut self, s: f32) -> Self { self.title_size = s; self }
50}
51
52const HEADER_H: f32 = 44.0;
53const PAD_H: f32 = 14.0;
54
55impl Widget for Accordion {
56 fn layout(&self, ctx: &LayoutCtx) -> Size {
57 let w = super::avail_w(ctx.constraints);
58 let mut h = HEADER_H;
59 if self.expanded.get() {
60 let bc = Constraints::loose(w - PAD_H * 2.0, f32::INFINITY);
61 h += self.body.layout(&ctx.with_constraints(bc)).height + 12.0;
62 }
63 ctx.constraints.constrain(Size { width: w, height: h })
64 }
65
66 fn paint(&self, ctx: &mut PaintCtx) {
67 let (bg, fg, outline, shadow) = {
69 let t = &ctx.theme.colors;
70 (
71 self.background.unwrap_or_else(|| ctx.tc(t.surface)),
72 ctx.tc(t.on_surface),
73 self.border.unwrap_or((ctx.tc(t.outline), 1.0)),
74 ctx.tc(t.shadow),
75 )
76 };
77 let r = ctx.rect;
78 let open = self.expanded.get();
79 let t = ctx.animate_to(if open { 1.0 } else { 0.0 }, 0.0);
82
83 if self.elevation > 0.0 {
89 ctx.fill_shadow_rrect(r, self.radius, Color::rgba(shadow.r, shadow.g, shadow.b, 90), 3.0 * self.elevation);
90 }
91 draw_rounded_rect_pub(ctx, r, bg, self.radius);
92 if outline.1 > 0.0 {
93 ctx.stroke_rrect(r, self.radius, outline.0, outline.1);
94 }
95
96 let header = Rect { origin: r.origin, size: Size { width: r.size.width, height: HEADER_H } };
98 let lh = ctx.font.line_height(self.title_size);
99 ctx.draw_text_at(
100 &self.title,
101 Point { x: r.origin.x + PAD_H, y: r.origin.y + (HEADER_H - lh) / 2.0 },
102 fg,
103 self.title_size,
104 );
105 let chev_size = self.title_size + 2.0;
114 let cx = r.origin.x + r.size.width - PAD_H - chev_size;
115 let cy = r.origin.y + (HEADER_H - chev_size) / 2.0;
116 let chev_rect = Rect { origin: Point { x: cx, y: cy }, size: Size { width: chev_size, height: chev_size } };
117 if t < 1.0 {
118 let a = (255.0 * (1.0 - t)) as u8;
119 super::Icon::new(super::IconKind::ChevronRight)
120 .size(chev_size)
121 .color(Color::rgba(fg.r, fg.g, fg.b, a))
122 .paint(&mut ctx.child(chev_rect));
123 }
124 if t > 0.0 {
125 let a = (255.0 * t) as u8;
126 super::Icon::new(super::IconKind::ChevronDown)
127 .size(chev_size)
128 .color(Color::rgba(fg.r, fg.g, fg.b, a))
129 .paint(&mut ctx.child(chev_rect));
130 }
131
132 let atom = self.expanded.clone();
133 let header_ctx = ctx.child(header);
134 header_ctx.semantics(
135 super::Semantics::new(rosace_core::Role::Button)
136 .label(&self.title)
137 .value(if open { "expanded" } else { "collapsed" }),
138 );
139 header_ctx.register_hit(std::sync::Arc::new(move || atom.set(!atom.get())));
140
141 if open {
142 let bc = Constraints::loose(r.size.width - PAD_H * 2.0, f32::INFINITY);
143 let bs = self.body.layout(&ctx.layout_ctx(bc));
144 let body_rect = Rect {
145 origin: Point { x: r.origin.x + PAD_H, y: r.origin.y + HEADER_H + 4.0 },
146 size: Size { width: r.size.width - PAD_H * 2.0, height: bs.height },
147 };
148 if t < 1.0 {
150 super::request_animation();
151 }
152 self.body.paint(&mut ctx.child(body_rect));
153 }
154 }
155}