teksilo_widgets/styles/
recipe_radio_style.rs1use teksilo_canvas::{Canvas, Rect, Size, SizeProposal};
18use teksilo_core::accessibility::AccessNodeBuilder;
19use teksilo_core::binding::BindingLevel;
20use teksilo_core::build_context::BuildContext;
21use teksilo_core::focus::FocusOrigin;
22use teksilo_core::signal::Signal;
23use teksilo_core::styles::{RadioStyle, RadioStyleConfig, RadioVariant};
24use teksilo_core::widget::{LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement};
25use teksilo_core::widget_id::WidgetId;
26use teksilo_tokens::{Color, CornerRadius};
27
28pub const RADIO_VISUAL_SIZE: f32 = 19.0;
30pub const RADIO_HIT_AREA: f32 = 24.0;
31pub const RADIO_LABEL_GAP: f32 = 6.0;
32pub const RADIO_INNER_DOT_SIZE: f32 = 7.0;
33
34#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct RadioRecipe {
39 pub visual_size: f32,
40 pub hit_area: f32,
41 pub label_gap: f32,
42 pub inner_dot_size: f32,
43}
44
45impl Default for RadioRecipe {
46 fn default() -> Self {
47 Self {
48 visual_size: RADIO_VISUAL_SIZE,
49 hit_area: RADIO_HIT_AREA,
50 label_gap: RADIO_LABEL_GAP,
51 inner_dot_size: RADIO_INNER_DOT_SIZE,
52 }
53 }
54}
55
56#[derive(Debug, Default, Clone, Copy)]
60pub struct RecipeRadioStyle {
61 pub recipe: RadioRecipe,
62}
63
64impl RecipeRadioStyle {
65 pub fn new(recipe: RadioRecipe) -> Self {
66 Self { recipe }
67 }
68}
69
70impl RadioStyle for RecipeRadioStyle {
71 fn make_body(&self, cfg: &RadioStyleConfig, ctx: &mut BuildContext) -> WidgetId {
72 let focus_origin = cfg
73 .is_focused
74 .zip(&cfg.is_hovered)
75 .map(|(focused, hovered)| {
76 if *focused {
77 Some(if *hovered {
78 FocusOrigin::Pointer
79 } else {
80 FocusOrigin::Keyboard
81 })
82 } else {
83 None
84 }
85 });
86
87 ctx.add(RadioBody {
88 is_selected: cfg.is_selected.clone(),
89 is_hovered: cfg.is_hovered.clone(),
90 is_pressed: cfg.is_pressed.clone(),
91 is_disabled: cfg.is_disabled.clone(),
92 focus_origin,
93 variant: cfg.variant,
94 recipe: self.recipe,
95 })
96 }
97}
98
99struct RadioBody {
100 is_selected: Signal<bool>,
101 is_hovered: Signal<bool>,
102 is_pressed: Signal<bool>,
103 is_disabled: Signal<bool>,
104 focus_origin: Signal<Option<FocusOrigin>>,
105 variant: RadioVariant,
106 recipe: RadioRecipe,
107}
108
109impl std::fmt::Debug for RadioBody {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.debug_struct("RadioBody")
112 .field("variant", &self.variant)
113 .finish()
114 }
115}
116
117impl Widget for RadioBody {
118 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
119 let id = ctx.self_id();
120 let registry = ctx.binding_registry();
121 self.is_selected
122 .bind_to(id, registry, BindingLevel::RepaintOnly);
123 self.is_hovered
124 .bind_to(id, registry, BindingLevel::RepaintOnly);
125 self.is_pressed
126 .bind_to(id, registry, BindingLevel::RepaintOnly);
127 self.is_disabled
128 .bind_to(id, registry, BindingLevel::RepaintOnly);
129 self.focus_origin
130 .bind_to(id, registry, BindingLevel::RepaintOnly);
131 vec![]
132 }
133
134 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
135 Size::new(self.recipe.visual_size, self.recipe.visual_size).into()
136 }
137
138 fn place_children(
139 &self,
140 _bounds: Rect,
141 _proposal: SizeProposal,
142 _children: &mut [WidgetPlacement],
143 _ctx: &LayoutContext,
144 ) {
145 }
146
147 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
148 let colors = &ctx.theme.colors;
149 let selected = self.is_selected.get();
150 let disabled = self.is_disabled.get();
151 let pressed = self.is_pressed.get();
152 let hovered = self.is_hovered.get();
153 let focused = self.focus_origin.get().is_some();
154
155 let border_color = if focused {
158 colors.border_focused
159 } else if disabled {
160 colors.accent_disabled
161 } else if selected {
162 colors.accent
163 } else if hovered {
164 colors.border_strong
165 } else {
166 colors.border
167 };
168
169 let border_width = if focused {
170 ctx.theme.shape.focus_ring_width
171 } else {
172 ctx.theme.shape.border_width
173 };
174
175 let outer_corner = match self.variant {
177 RadioVariant::Circle => CornerRadius::uniform(self.recipe.visual_size / 2.0),
178 RadioVariant::Square => CornerRadius::uniform(0.0),
179 RadioVariant::Rounded => CornerRadius::uniform(self.recipe.visual_size * 0.25),
180 };
181
182 canvas.fill_rounded_rect(bounds, outer_corner, Color::TRANSPARENT);
184 if border_width > 0.0 && border_color != Color::TRANSPARENT {
185 canvas.stroke_rounded_rect(bounds, outer_corner, border_color, border_width);
186 }
187
188 if !selected {
192 return;
193 }
194 let dot_color = if disabled {
195 colors.accent_disabled
196 } else if pressed {
197 colors.accent_pressed
198 } else if hovered {
199 colors.accent_hover
200 } else {
201 colors.accent
202 };
203 let dot_size = self.recipe.inner_dot_size;
204 let dot_rect = Rect::new(
205 bounds.x + (bounds.width - dot_size) / 2.0,
206 bounds.y + (bounds.height - dot_size) / 2.0,
207 dot_size,
208 dot_size,
209 );
210 let dot_corner = match self.variant {
211 RadioVariant::Circle => CornerRadius::uniform(dot_size / 2.0),
212 RadioVariant::Square => CornerRadius::uniform(0.0),
213 RadioVariant::Rounded => CornerRadius::uniform(dot_size * 0.25),
214 };
215 canvas.fill_rounded_rect(dot_rect, dot_corner, dot_color);
216 }
217
218 fn accessibility(&self, _builder: &mut AccessNodeBuilder) {
219 }
222}