freya_components/
checkbox.rs1use freya_animation::prelude::*;
2use freya_core::prelude::*;
3use torin::prelude::*;
4
5use crate::{
6 define_theme,
7 get_theme,
8 icons::{
9 IconThemePartialExt,
10 tick::TickIcon,
11 },
12};
13
14define_theme! {
15 %[component]
16 pub Checkbox {
17 %[fields]
18 unselected_fill: Color,
19 selected_fill: Color,
20 selected_icon_fill: Color,
21 border_fill: Color,
22 }
23}
24
25#[derive(Clone, PartialEq)]
54pub struct Checkbox {
55 pub(crate) theme: Option<CheckboxThemePartial>,
56 selected: bool,
57 key: DiffKey,
58 size: f32,
59}
60
61impl KeyExt for Checkbox {
62 fn key_mut(&mut self) -> &mut DiffKey {
63 &mut self.key
64 }
65}
66
67impl Default for Checkbox {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl Checkbox {
74 pub fn new() -> Self {
75 Self {
76 selected: false,
77 theme: None,
78 key: DiffKey::None,
79 size: 20.,
80 }
81 }
82
83 pub fn selected(mut self, selected: bool) -> Self {
84 self.selected = selected;
85 self
86 }
87
88 pub fn theme(mut self, theme: CheckboxThemePartial) -> Self {
89 self.theme = Some(theme);
90 self
91 }
92
93 pub fn size(mut self, size: f32) -> Self {
94 self.size = size;
95 self
96 }
97}
98
99impl Component for Checkbox {
100 fn render(&self) -> impl IntoElement {
101 let a11y_id = use_a11y();
102 let focus = use_focus(a11y_id);
103 let CheckboxTheme {
104 border_fill,
105 unselected_fill,
106 selected_fill,
107 selected_icon_fill,
108 } = get_theme!(&self.theme, CheckboxThemePreference, "checkbox");
109
110 let animation = use_animation_with_dependencies(&self.selected, move |conf, selected| {
111 conf.on_change(OnChange::Rerun);
112 conf.on_creation(OnCreation::Finish);
113
114 let scale = AnimNum::new(0.6, 1.)
115 .time(350)
116 .ease(Ease::Out)
117 .function(Function::Expo);
118 let opacity = AnimNum::new(0., 1.)
119 .time(350)
120 .ease(Ease::Out)
121 .function(Function::Expo);
122
123 if *selected {
124 (scale, opacity)
125 } else {
126 (scale.into_reversed(), opacity.into_reversed())
127 }
128 });
129
130 let (scale, opacity) = animation.read().value();
131
132 let (background, fill) = if self.selected {
133 (selected_fill, selected_fill)
134 } else {
135 (Color::TRANSPARENT, unselected_fill)
136 };
137
138 let border = Border::new()
139 .fill(fill)
140 .width(2.)
141 .alignment(BorderAlignment::Inner);
142
143 let focused_border = (focus() == Focus::Keyboard).then(|| {
144 Border::new()
145 .fill(border_fill)
146 .width((self.size * 0.15).ceil())
147 .alignment(BorderAlignment::Outer)
148 });
149
150 rect()
151 .a11y_id(a11y_id)
152 .a11y_focusable(Focusable::Enabled)
153 .a11y_role(AccessibilityRole::CheckBox)
154 .width(Size::px(self.size))
155 .height(Size::px(self.size))
156 .padding(Gaps::new_all(4.0))
157 .main_align(Alignment::center())
158 .cross_align(Alignment::center())
159 .corner_radius(CornerRadius::new_all(self.size * 0.24))
160 .border(border)
161 .border(focused_border)
162 .background(background)
163 .on_key_down({
164 move |e: Event<KeyboardEventData>| {
165 if !e.is_press_event() {
166 e.stop_propagation();
167 }
168 }
169 })
170 .maybe_child((self.selected || opacity > 0.).then(|| {
171 rect().opacity(opacity).scale(scale).child(
172 TickIcon::new()
173 .width(Size::px(self.size * 0.7))
174 .height(Size::px(self.size * 0.7))
175 .fill(selected_icon_fill),
176 )
177 }))
178 }
179
180 fn render_key(&self) -> DiffKey {
181 self.key.clone().or(self.default_key())
182 }
183}