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