rosace_widgets/tree/
fab.rs1use std::sync::Arc;
10
11use rosace_core::types::{Point, Rect, Size};
12use rosace_layout::Constraints;
13use rosace_render::Color;
14
15use super::button::lighten;
16use super::container::draw_rounded_rect_pub;
17use super::{LayoutCtx, PaintCtx, Widget};
18
19pub struct FloatingActionButton {
22 icon: Option<super::BoxedWidget>,
23 label: Option<String>,
24 size: f32,
25 background: Option<Color>,
26 foreground: Option<Color>,
27 radius: Option<f32>,
30 elevation: f32,
32 disabled: bool,
33 on_press: Option<Arc<dyn Fn() + Send + Sync>>,
34}
35
36impl FloatingActionButton {
37 pub fn new() -> Self {
38 Self {
39 icon: None,
40 label: None,
41 size: 56.0,
42 background: None,
43 foreground: None,
44 radius: None,
45 elevation: 1.0,
46 disabled: false,
47 on_press: None,
48 }
49 }
50 pub fn icon(mut self, w: impl Widget + 'static) -> Self {
52 self.icon = Some(Box::new(w));
53 self
54 }
55 pub fn label(mut self, s: impl Into<String>) -> Self {
57 self.label = Some(s.into());
58 self
59 }
60 pub fn size(mut self, s: f32) -> Self { self.size = s; self }
61 pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
63 pub fn color(mut self, c: Color) -> Self { self.foreground = Some(c); self }
65 pub fn radius(mut self, r: f32) -> Self { self.radius = Some(r); self }
67 pub fn elevation(mut self, e: f32) -> Self { self.elevation = e; self }
68 pub fn disabled(mut self, d: bool) -> Self { self.disabled = d; self }
69 pub fn on_press(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
70 self.on_press = Some(Arc::new(f));
71 self
72 }
73}
74
75impl Default for FloatingActionButton {
76 fn default() -> Self { Self::new() }
77}
78
79impl Widget for FloatingActionButton {
80 fn layout(&self, _ctx: &LayoutCtx) -> Size {
81 Size { width: self.size, height: self.size }
82 }
83
84 fn paint(&self, ctx: &mut PaintCtx) {
85 let (bg, fg, shadow) = {
87 let t = &ctx.theme.colors;
88 (
89 self.background.unwrap_or_else(|| ctx.tc(t.primary)),
90 self.foreground.unwrap_or_else(|| ctx.tc(t.on_primary)),
91 ctx.tc(t.shadow),
92 )
93 };
94 let radius = self.radius.unwrap_or(self.size / 2.0);
95 let r = ctx.rect;
96
97 let sem_label = self.label.clone().unwrap_or_else(|| "action".to_string());
98 ctx.semantics(super::Semantics::new(rosace_core::Role::Button).label(&sem_label));
99
100 if self.elevation > 0.0 && !self.disabled {
106 ctx.fill_shadow_rrect(
107 r,
108 radius,
109 Color::rgba(shadow.r, shadow.g, shadow.b, 90),
110 3.0 * self.elevation,
111 );
112 }
113
114 let target = if self.disabled { 0.0 } else if ctx.pressed() { 1.0 } else if ctx.hovered() { 0.5 } else { 0.0 };
116 let emphasis = ctx.animate_to(target, 0.0);
117 let bg = if self.disabled {
118 Color::rgba(bg.r, bg.g, bg.b, 110)
119 } else if emphasis > 0.0 {
120 lighten(bg, (0.12 * emphasis * 2.0).min(1.0))
121 } else {
122 bg
123 };
124 draw_rounded_rect_pub(ctx, r, bg, radius);
125
126 if let Some(icon) = &self.icon {
127 let inner = self.size * 0.45;
128 let is = icon.layout(&ctx.layout_ctx(Constraints::loose(inner, inner)));
129 icon.paint(&mut ctx.child(Rect {
130 origin: Point {
131 x: r.origin.x + (r.size.width - is.width) / 2.0,
132 y: r.origin.y + (r.size.height - is.height) / 2.0,
133 },
134 size: is,
135 }));
136 } else {
137 let text = self.label.as_deref().unwrap_or("+");
138 let px = self.size * 0.4;
139 let tw = ctx.font.measure_text(text, px);
140 let lh = ctx.font.line_height(px);
141 ctx.draw_text_at(
142 text,
143 Point {
144 x: r.origin.x + (r.size.width - tw) / 2.0,
145 y: r.origin.y + (r.size.height - lh) / 2.0,
146 },
147 fg,
148 px,
149 );
150 }
151
152 if let Some(cb) = &self.on_press {
153 if !self.disabled {
154 ctx.register_hit(Arc::clone(cb));
155 }
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn fab_is_square_at_its_configured_size() {
166 let fab = FloatingActionButton::new().size(64.0);
167 let font = rosace_render::FontCache::embedded();
168 let theme = rosace_theme::built_in::dark_theme();
169 let ctx = LayoutCtx::new(Constraints::loose(400.0, 400.0), &font, &theme);
170 let size = fab.layout(&ctx);
171 assert_eq!((size.width, size.height), (64.0, 64.0));
172 }
173
174 #[test]
175 fn default_size_is_the_material_convention() {
176 let fab = FloatingActionButton::new();
177 let font = rosace_render::FontCache::embedded();
178 let theme = rosace_theme::built_in::dark_theme();
179 let ctx = LayoutCtx::new(Constraints::loose(400.0, 400.0), &font, &theme);
180 assert_eq!(fab.layout(&ctx).width, 56.0);
181 }
182}