1use ferroui_base::input::InputElementImpl;
9use ferroui_base::interactivity::InteractiveImpl;
10use ferroui_base::layout::{Layoutable, LayoutableImpl};
11use ferroui_base::{
12 AttachedProperty, FerroObjectImpl, FerroProperty, Rect, Ref, Size, StyledElementImpl,
13 StyledProperty, Thickness, VisualImpl, ferro_class, ferro_class_info, ferro_impl_classes,
14 ferro_properties, instantiate,
15};
16use ferroui_controls::{Control, ControlImpl, Panel, PanelImpl, TextBlock};
17
18#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
20pub enum MaterialTextFieldSlot {
21 Container,
22 Label,
23 Leading,
24 Trailing,
25 Prefix,
26 Suffix,
27 #[default]
28 Editor,
29 Placeholder,
30 Supporting,
31 InnerLeading,
32 InnerTrailing,
33 Clear,
34 Reveal,
35}
36ferroui_base::ferro_markup_enum!(
37 MaterialTextFieldSlot {
38 Container, Label, Leading, Trailing, Prefix, Suffix, Editor, Placeholder,
39 Supporting, InnerLeading, InnerTrailing, Clear, Reveal
40 },
41 { namespace: "FerroUI.Themes.Material" }
42);
43
44#[repr(C)]
49pub struct MaterialTextFieldLayout {
50 base: Panel,
51}
52ferro_class!(MaterialTextFieldLayout: Panel);
53ferro_class_info!(MaterialTextFieldLayout {
54 new: MaterialTextFieldLayout::new
55});
56ferro_impl_classes!(MaterialTextFieldLayout:
57 FerroObjectImpl, StyledElementImpl, VisualImpl, InteractiveImpl, InputElementImpl,
58 ControlImpl, PanelImpl
59);
60
61ferro_properties! { impl MaterialTextFieldLayout {
62 pub fn slot_property() -> AttachedProperty<MaterialTextFieldSlot> {
63 FerroProperty::register_attached::<MaterialTextFieldLayout, Control, _>(
64 "Slot", MaterialTextFieldSlot::Editor)
65 }
66 pub fn label_progress_property() -> StyledProperty<f64> {
68 FerroProperty::register::<MaterialTextFieldLayout, _>("LabelProgress", 0.0)
69 }
70 pub fn is_outlined_property() -> StyledProperty<bool> {
71 FerroProperty::register::<MaterialTextFieldLayout, _>("IsOutlined", false)
72 }
73 pub fn single_line_property() -> StyledProperty<bool> {
74 FerroProperty::register::<MaterialTextFieldLayout, _>("SingleLine", true)
75 }
76 pub fn content_padding_property() -> StyledProperty<Thickness> {
77 FerroProperty::register::<MaterialTextFieldLayout, _>("ContentPadding", Thickness::uniform(16.0))
78 }
79 pub fn container_min_height_property() -> StyledProperty<f64> {
80 FerroProperty::register::<MaterialTextFieldLayout, _>("ContainerMinHeight", 56.0)
81 }
82} }
83
84type Slots = [Option<Ref<Control>>; 13];
85
86impl MaterialTextFieldLayout {
87 fn static_constructor() {
88 Layoutable::affects_measure::<Self>(&[
89 Self::label_progress_property().as_property(),
90 Self::is_outlined_property().as_property(),
91 Self::single_line_property().as_property(),
92 Self::content_padding_property().as_property(),
93 Self::container_min_height_property().as_property(),
94 ]);
95 Panel::affects_parent_measure::<Self>(&[Self::slot_property().as_property()]);
96 }
97 pub fn construct() -> Self {
98 Self {
99 base: Panel::construct(),
100 }
101 }
102 pub fn new() -> Ref<Self> {
103 instantiate(Self::construct())
104 }
105 pub fn get_slot(child: &Control) -> MaterialTextFieldSlot {
106 child.get_value(Self::slot_property())
107 }
108 pub fn set_slot(child: &Control, value: MaterialTextFieldSlot) {
109 child.set_value(Self::slot_property(), value);
110 }
111 pub fn label_progress(&self) -> f64 {
112 self.get_value(Self::label_progress_property())
113 }
114 pub fn set_label_progress(&self, value: f64) {
115 self.set_value(Self::label_progress_property(), value);
116 }
117 pub fn is_outlined(&self) -> bool {
118 self.get_value(Self::is_outlined_property())
119 }
120 pub fn set_is_outlined(&self, value: bool) {
121 self.set_value(Self::is_outlined_property(), value);
122 }
123 pub fn single_line(&self) -> bool {
124 self.get_value(Self::single_line_property())
125 }
126 pub fn set_single_line(&self, value: bool) {
127 self.set_value(Self::single_line_property(), value);
128 }
129 pub fn content_padding(&self) -> Thickness {
130 self.get_value(Self::content_padding_property())
131 }
132 pub fn set_content_padding(&self, value: Thickness) {
133 self.set_value(Self::content_padding_property(), value);
134 }
135 pub fn container_min_height(&self) -> f64 {
136 self.get_value(Self::container_min_height_property())
137 }
138 pub fn set_container_min_height(&self, value: f64) {
139 self.set_value(Self::container_min_height_property(), value);
140 }
141
142 fn slots(&self) -> Slots {
143 let mut slots = std::array::from_fn(|_| None);
144 for child in self.children().snapshot().iter() {
145 let slot = Self::get_slot(&child) as usize;
146 slots[slot] = Some(child.clone());
147 }
148 slots
149 }
150
151 fn progress(&self) -> f64 {
152 let p = self.label_progress();
153 if p.is_finite() {
154 p.clamp(-0.25, 1.25)
155 } else {
156 0.0
157 }
158 }
159
160 fn label_typography(&self, slots: &Slots) {
161 if let Some(label) = slots[MaterialTextFieldSlot::Label as usize]
162 .as_ref()
163 .and_then(|label| label.cast::<TextBlock>())
164 {
165 let p = self.progress();
166 label.set_current_value(TextBlock::font_size_property(), 16.0 - 4.0 * p);
167 label.set_current_value(TextBlock::line_height_property(), 24.0 - 8.0 * p);
168 label.set_current_value(TextBlock::letter_spacing_property(), 0.5 - 0.1 * p);
169 }
170 }
171
172 fn padding(&self, slots: &Slots) -> Thickness {
173 use MaterialTextFieldSlot::*;
174 let p = self.content_padding();
175 let label_inset = if visible(slots, Label) && !self.is_outlined() {
176 8.0 * self.progress().clamp(0.0, 1.0)
177 } else {
178 0.0
179 };
180 Thickness::new(
181 (positive(p.left) - if visible(slots, Leading) { 12.0 } else { 0.0 }).max(0.0),
182 positive(p.top) + label_inset,
183 (positive(p.right) - if visible(slots, Trailing) { 12.0 } else { 0.0 }).max(0.0),
184 (positive(p.bottom) - label_inset).max(0.0),
185 )
186 }
187}
188
189impl LayoutableImpl for MaterialTextFieldLayout {
190 fn measure_override(this: &Self, available: Size) -> Size {
191 use MaterialTextFieldSlot::*;
192 let slots = this.slots();
193 this.label_typography(&slots);
194 let padding = this.padding(&slots);
195 let mut remaining = available.width.max(0.0);
196 let mut icons_width = 0.0_f64;
197 let mut icons_height = 0.0_f64;
198 for slot in [
199 InnerLeading,
200 Leading,
201 InnerTrailing,
202 Trailing,
203 Reveal,
204 Clear,
205 ] {
206 measure(&slots, slot, Size::new(remaining, available.height));
207 let size = desired(&slots, slot);
208 icons_width += size.width;
209 icons_height = icons_height.max(size.height);
210 remaining = (remaining - size.width).max(0.0);
211 }
212 let expanded_label_width = (remaining - padding.left - padding.right).max(0.0);
213 let label_width = if this.is_outlined() && available.width.is_finite() {
214 let minimized_width = (available.width
215 - positive(this.content_padding().left)
216 - positive(this.content_padding().right))
217 .max(0.0);
218 expanded_label_width
219 + (minimized_width - expanded_label_width) * this.progress().clamp(0.0, 1.0)
220 } else {
221 expanded_label_width
222 };
223 measure(&slots, Label, Size::new(label_width, available.height));
224 remaining = (remaining - padding.left - padding.right).max(0.0);
225 let mut affix_width = 0.0_f64;
226 for slot in [Prefix, Suffix] {
227 measure(&slots, slot, Size::new(remaining, available.height));
228 if visible(&slots, slot) {
229 let width = desired(&slots, slot).width + 2.0;
230 affix_width += width;
231 remaining = (remaining - width).max(0.0);
232 }
233 }
234 let editor_available = Size::new(
235 remaining,
236 (available.height - padding.top - padding.bottom).max(0.0),
237 );
238 measure(&slots, Editor, editor_available);
239 measure(&slots, Placeholder, editor_available);
240 let editor = desired(&slots, Editor);
241 let placeholder = desired(&slots, Placeholder);
242 let line_height = editor
243 .height
244 .max(placeholder.height)
245 .max(desired(&slots, Prefix).height)
246 .max(desired(&slots, Suffix).height)
247 .max(24.0);
248 let field_height = positive(this.container_min_height())
249 .max(icons_height)
250 .max(line_height + padding.top + padding.bottom);
251 let text_width = icons_width
252 + affix_width
253 + editor.width.max(placeholder.width)
254 + padding.left
255 + padding.right;
256 let label_width = desired(&slots, Label).width
257 + if this.is_outlined() {
258 positive(this.content_padding().left) + positive(this.content_padding().right)
259 } else {
260 icons_width + padding.left + padding.right
261 };
262 measure(&slots, Container, Size::new(available.width, field_height));
263 let width = text_width
264 .max(label_width)
265 .max(desired(&slots, Container).width);
266 let support_width = (available.width - 32.0).max(0.0);
267 measure(
268 &slots,
269 Supporting,
270 Size::new(support_width, available.height),
271 );
272 let supporting = desired(&slots, Supporting);
273 let top = if this.is_outlined() && visible(&slots, Label) {
276 8.0
277 } else {
278 0.0
279 };
280 let bottom = if visible(&slots, Supporting) {
281 supporting.height.max(16.0) + 4.0
282 } else {
283 0.0
284 };
285 Size::new(
286 width.max(if visible(&slots, Supporting) {
287 supporting.width + 32.0
288 } else {
289 0.0
290 }),
291 field_height + top + bottom,
292 )
293 }
294
295 fn arrange_override(this: &Self, final_size: Size) -> Size {
296 use MaterialTextFieldSlot::*;
297 let slots = this.slots();
298 let padding = this.padding(&slots);
299 let has_label = visible(&slots, Label);
300 let top = if this.is_outlined() && has_label {
301 8.0
302 } else {
303 0.0
304 };
305 let support_height = if visible(&slots, Supporting) {
306 desired(&slots, Supporting).height.max(16.0)
307 } else {
308 0.0
309 };
310 let support_gap = if visible(&slots, Supporting) {
311 4.0
312 } else {
313 0.0
314 };
315 let field_height = (final_size.height - top - support_height - support_gap).max(0.0);
316 arrange(
317 &slots,
318 Container,
319 Rect::new(0.0, top, final_size.width, field_height),
320 );
321 let mut start = 0.0;
322 let mut end = final_size.width;
323 for slot in [InnerLeading, Leading] {
324 let size = desired(&slots, slot);
325 arrange(
326 &slots,
327 slot,
328 Rect::new(
329 start,
330 top + (field_height - size.height) / 2.0,
331 size.width,
332 size.height,
333 ),
334 );
335 start += size.width;
336 }
337 for slot in [InnerTrailing, Trailing, Reveal, Clear] {
338 let size = desired(&slots, slot);
339 end -= size.width;
340 arrange(
341 &slots,
342 slot,
343 Rect::new(
344 end,
345 top + (field_height - size.height) / 2.0,
346 size.width,
347 size.height,
348 ),
349 );
350 }
351 let label_start = start + padding.left;
352 start += padding.left;
353 end -= padding.right;
354 let content_height = (field_height - padding.top - padding.bottom).max(0.0);
355 let y = top + padding.top;
356 for slot in [Prefix, Suffix] {
357 let size = desired(&slots, slot);
358 let slot_x = if slot == Prefix {
359 start
360 } else {
361 end - size.width
362 };
363 let slot_y = if this.single_line() {
364 y + (content_height - size.height) / 2.0
365 } else {
366 y
367 };
368 arrange(
369 &slots,
370 slot,
371 Rect::new(slot_x, slot_y, size.width, size.height),
372 );
373 if visible(&slots, slot) {
374 if slot == Prefix {
375 start += size.width + 2.0;
376 } else {
377 end -= size.width + 2.0;
378 }
379 }
380 }
381 let editor_rect = Rect::new(start, y, (end - start).max(0.0), content_height);
382 arrange(&slots, Editor, editor_rect);
383 arrange(&slots, Placeholder, editor_rect);
384 let label = desired(&slots, Label);
385 let p = this.progress();
386 let start_y = if this.single_line() {
387 (field_height - label.height) / 2.0
388 } else {
389 positive(this.content_padding().top)
390 };
391 let target_y = if this.is_outlined() {
392 -label.height / 2.0
393 } else {
394 8.0
395 };
396 let target_x = if this.is_outlined() {
397 positive(this.content_padding().left)
398 } else {
399 label_start
400 };
401 let label_rect = Rect::new(
402 label_start + (target_x - label_start) * p,
403 top + start_y + (target_y - start_y) * p,
404 label.width,
405 label.height,
406 );
407 arrange(&slots, Label, label_rect);
408 if let Some(border) = slots[Container as usize]
409 .as_ref()
410 .and_then(|control| control.cast::<crate::MaterialTextFieldBorder>())
411 {
412 border.set_notch_start(label_rect.x);
413 border.set_notch_width(label_rect.width);
414 border.set_notch_open(if this.is_outlined() && has_label {
415 p
416 } else {
417 0.0
418 });
419 }
420 arrange(
421 &slots,
422 Supporting,
423 Rect::new(
424 16.0,
425 top + field_height + support_gap,
426 (final_size.width - 32.0).max(0.0),
427 support_height,
428 ),
429 );
430 final_size
431 }
432}
433
434fn positive(value: f64) -> f64 {
435 if value.is_finite() {
436 value.max(0.0)
437 } else {
438 0.0
439 }
440}
441fn visible(slots: &Slots, slot: MaterialTextFieldSlot) -> bool {
442 slots[slot as usize]
443 .as_ref()
444 .is_some_and(|child| child.is_visible())
445}
446fn desired(slots: &Slots, slot: MaterialTextFieldSlot) -> Size {
447 slots[slot as usize]
448 .as_ref()
449 .map_or(Size::default(), |child| child.desired_size())
450}
451fn measure(slots: &Slots, slot: MaterialTextFieldSlot, available: Size) {
452 if let Some(child) = &slots[slot as usize] {
453 child.measure(available);
454 }
455}
456fn arrange(slots: &Slots, slot: MaterialTextFieldSlot, rect: Rect) {
457 if let Some(child) = &slots[slot as usize] {
458 child.arrange(rect);
459 }
460}