teksilo_widgets/table_view/
filter.rs1use teksilo_canvas::{Rect, Size, SizeProposal};
19use teksilo_core::accessibility::AccessNodeBuilder;
20use teksilo_core::widget::{LayoutContext, PaintContext, Widget};
21use teksilo_tokens::TextRole;
22
23pub(crate) struct FilterIndicator {
27 size: f32,
28 active: bool,
29}
30
31impl FilterIndicator {
32 pub(crate) fn new(size: f32, active: bool) -> Self {
33 Self { size, active }
34 }
35}
36
37impl std::fmt::Debug for FilterIndicator {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("FilterIndicator")
40 .field("size", &self.size)
41 .field("active", &self.active)
42 .finish()
43 }
44}
45
46impl Widget for FilterIndicator {
47 fn layout_response(
48 &self,
49 _proposal: SizeProposal,
50 _ctx: &LayoutContext,
51 ) -> teksilo_core::widget::LayoutResponse {
52 Size::new(self.size, self.size).into()
53 }
54
55 fn paint(&self, bounds: Rect, canvas: &mut teksilo_canvas::Canvas, ctx: &PaintContext) {
56 let color = if self.active {
57 TextRole::Accent.resolve(&ctx.theme.colors)
58 } else {
59 TextRole::Secondary.resolve(&ctx.theme.colors)
60 };
61 let cx = bounds.x + bounds.width * 0.5;
65 let cy = bounds.y + bounds.height * 0.5;
66 let r = bounds.width.min(bounds.height) * 0.42;
67 canvas.fill_rect(
69 Rect::new(cx - r, cy - r, r * 2.0, (r * 0.30).max(1.0)),
70 color,
71 );
72 let stem_h = (r * 0.45).max(1.0);
74 let stem_w = (r * 0.30).max(1.0);
75 canvas.fill_rect(
76 Rect::new(cx - stem_w * 0.5, cy - r * 0.4, stem_w, r * 1.05),
77 color,
78 );
79 canvas.fill_rect(
81 Rect::new(
82 cx - (r * 0.14).max(0.5),
83 cy + r * 0.4,
84 (r * 0.28).max(1.0),
85 stem_h,
86 ),
87 color,
88 );
89 }
90
91 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
92 builder.set_hidden();
93 }
94}
95
96pub(crate) use rich::FilterPopoverContent;
97
98mod rich {
99 use std::cell::Cell;
100 use std::rc::Rc;
101 use teksilo_i18n::lit;
102
103 use teksilo_canvas::{Rect, SizeProposal};
104 use teksilo_core::build_context::BuildContext;
105 use teksilo_core::signal::Signal;
106 use teksilo_core::widget::{LayoutContext, Widget, WidgetPlacement};
107 use teksilo_core::widget_id::WidgetId;
108
109 use crate::icon_button::IconButton;
110 use crate::text_input::TextInput;
111
112 pub(crate) struct FilterPopoverContent {
117 text: Signal<String>,
118 placeholder: String,
119 #[allow(clippy::type_complexity)]
120 on_change: Option<Rc<dyn Fn(&str)>>,
121 focus_slot: Option<Rc<Cell<Option<WidgetId>>>>,
125 root_child_id: Option<WidgetId>,
126 }
127
128 impl FilterPopoverContent {
129 pub(crate) fn new(initial: impl Into<String>) -> Self {
130 Self {
131 text: Signal::new(initial.into()),
132 placeholder: String::from("Filter…"),
133 on_change: None,
134 focus_slot: None,
135 root_child_id: None,
136 }
137 }
138
139 #[allow(dead_code)]
140 pub(crate) fn placeholder(mut self, text: impl Into<String>) -> Self {
141 self.placeholder = text.into();
142 self
143 }
144
145 pub(crate) fn on_change(mut self, f: impl Fn(&str) + 'static) -> Self {
146 self.on_change = Some(Rc::new(f));
147 self
148 }
149
150 pub(crate) fn focus_slot(mut self, slot: Rc<Cell<Option<WidgetId>>>) -> Self {
151 self.focus_slot = Some(slot);
152 self
153 }
154 }
155
156 impl std::fmt::Debug for FilterPopoverContent {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 f.debug_struct("FilterPopoverContent")
159 .field("text_len", &self.text.get().len())
160 .field("placeholder", &self.placeholder)
161 .finish()
162 }
163 }
164
165 impl Widget for FilterPopoverContent {
166 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
167 let text = self.text.clone();
168 let commit = self.on_change.clone();
169
170 let clear = IconButton::clear().embedded().on_activate_fn({
174 let text = text.clone();
175 let commit = commit.clone();
176 move |_| {
177 text.set(String::new());
178 if let Some(cb) = commit.as_ref() {
179 cb("");
180 }
181 }
182 });
183
184 let mut input = TextInput::new(text.clone())
185 .placeholder(lit!(self.placeholder.clone()))
186 .trailing_slot(clear);
187
188 if let Some(cb) = commit {
195 let text = text.clone();
196 input = input.on_submit_fn(move |_ctx| {
197 cb(&text.get());
198 });
199 }
200
201 let input_id = ctx.add(input);
202 if let Some(slot) = &self.focus_slot {
203 slot.set(Some(input_id));
204 }
205
206 self.root_child_id = Some(input_id);
207 vec![input_id]
208 }
209
210 fn layout_response(
211 &self,
212 proposal: SizeProposal,
213 ctx: &LayoutContext,
214 ) -> teksilo_core::widget::LayoutResponse {
215 self.root_child_id
216 .and_then(|id| ctx.child_size(id, proposal))
217 .unwrap_or_else(|| proposal.resolve(280.0, 32.0))
218 .into()
219 }
220
221 fn place_children(
222 &self,
223 bounds: Rect,
224 _proposal: SizeProposal,
225 children: &mut [WidgetPlacement],
226 _ctx: &LayoutContext,
227 ) {
228 for child in children.iter_mut() {
229 child.origin = bounds.origin();
230 child.size = bounds.size();
231 }
232 }
233
234 fn children(&self) -> Vec<WidgetId> {
235 self.root_child_id.into_iter().collect()
236 }
237 }
238}