Skip to main content

ferroui_themes_material/
text_field_border.rs

1//! A Material outlined text-field border with a real label cutout. The standard
2//! FerroUI Border renderer still owns background sizing, rounded corners, layout
3//! rounding and shadows; only the outline is clipped around the floating label.
4//! Cutout padding follows AndroidX OutlinedTextField.kt, pinned to
5//! e4d933c98df7baa7ea2b83eea6e086915961e0ff (Apache-2.0).
6
7use ferroui_base::input::InputElementImpl;
8use ferroui_base::interactivity::InteractiveImpl;
9use ferroui_base::layout::LayoutableImpl;
10use ferroui_base::media::{
11    BoxShadows, CombinedGeometry, DrawingContext, GeometryCombineMode, RectangleGeometry,
12};
13use ferroui_base::{
14    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate,
15    FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Rect, Ref,
16    StaticType, StyledElementImpl, StyledProperty, TypeInfo, VisualImpl, VisualImplExt,
17};
18use ferroui_controls::utils::BorderRenderHelper;
19use ferroui_controls::{Border, ControlImpl};
20use std::cell::RefCell;
21
22#[repr(C)]
23pub struct MaterialTextFieldBorder {
24    base: Border,
25    background_renderer: RefCell<BorderRenderHelper>,
26    outline_renderer: RefCell<BorderRenderHelper>,
27}
28ferro_class!(MaterialTextFieldBorder:Border);
29ferro_class_info!(MaterialTextFieldBorder {
30    new: MaterialTextFieldBorder::new
31});
32ferro_impl_classes!(MaterialTextFieldBorder:InteractiveImpl,InputElementImpl,LayoutableImpl,ControlImpl);
33impl StyledElementImpl for MaterialTextFieldBorder {
34    fn style_key_override(_this: &Self) -> &'static TypeInfo {
35        // Retain Border#BorderElement selectors in TextBox-derived editor themes
36        // and application styles. The cutout properties use explicit owners.
37        <Border as StaticType>::TYPE
38    }
39}
40impl FerroObjectImpl for MaterialTextFieldBorder {
41    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
42        Self::parent_on_property_changed(this, change);
43        this.invalidate_visual();
44    }
45}
46impl VisualImpl for MaterialTextFieldBorder {
47    fn render(this: &Self, dc: &mut DrawingContext) {
48        let size = this.bounds().size();
49        let Some((start, end)) = notch_range(
50            this.notch_start(),
51            this.notch_width(),
52            this.notch_open(),
53            size.width,
54        ) else {
55            Self::parent_render(this, dc);
56            return;
57        };
58        let thickness = this.layout_thickness();
59        let corners = this.corner_radius();
60        let sizing = this.background_sizing();
61        // Paint the background and any shadow before clipping; an outlined field
62        // remains correct on any parent surface, including translucent brushes.
63        this.background_renderer.borrow_mut().render(
64            dc,
65            size,
66            thickness,
67            corners,
68            sizing,
69            this.background().as_ref(),
70            None,
71            &this.box_shadow(),
72        );
73        let full = RectangleGeometry::with_rect(Rect::from_size(size));
74        let cut = RectangleGeometry::with_rect(Rect::new(
75            start,
76            -1.0,
77            end - start,
78            thickness.top.max(0.0) + 2.0,
79        ));
80        let clip = CombinedGeometry::with_mode(
81            GeometryCombineMode::Exclude,
82            Some(full.upcast()),
83            Some(cut.upcast()),
84        );
85        let state = dc.push_geometry_clip(&clip.upcast());
86        this.outline_renderer.borrow_mut().render(
87            dc,
88            size,
89            thickness,
90            corners,
91            sizing,
92            None,
93            this.border_brush().as_ref(),
94            &BoxShadows::default(),
95        );
96        dc.pop(state);
97    }
98}
99ferro_properties! {impl MaterialTextFieldBorder {
100    pub fn notch_start_property()->StyledProperty<f64>{FerroProperty::register::<MaterialTextFieldBorder,_>("NotchStart",0.0)}
101    pub fn notch_width_property()->StyledProperty<f64>{FerroProperty::register::<MaterialTextFieldBorder,_>("NotchWidth",0.0)}
102    pub fn notch_open_property()->StyledProperty<f64>{FerroProperty::register::<MaterialTextFieldBorder,_>("NotchOpen",0.0)}
103}}
104impl MaterialTextFieldBorder {
105    pub fn construct() -> Self {
106        Self {
107            base: Border::construct(),
108            background_renderer: RefCell::new(BorderRenderHelper::new()),
109            outline_renderer: RefCell::new(BorderRenderHelper::new()),
110        }
111    }
112    pub fn new() -> Ref<Self> {
113        instantiate(Self::construct())
114    }
115    pub fn notch_start(&self) -> f64 {
116        self.get_value(Self::notch_start_property())
117    }
118    pub fn set_notch_start(&self, value: f64) {
119        self.set_value(Self::notch_start_property(), value);
120    }
121    pub fn notch_width(&self) -> f64 {
122        self.get_value(Self::notch_width_property())
123    }
124    pub fn set_notch_width(&self, value: f64) {
125        self.set_value(Self::notch_width_property(), value);
126    }
127    pub fn notch_open(&self) -> f64 {
128        self.get_value(Self::notch_open_property())
129    }
130    pub fn set_notch_open(&self, value: f64) {
131        self.set_value(Self::notch_open_property(), value);
132    }
133}
134fn notch_range(start: f64, width: f64, open: f64, total: f64) -> Option<(f64, f64)> {
135    if ![start, width, open, total]
136        .iter()
137        .all(|value| value.is_finite())
138        || width <= 0.0
139        || total <= 0.0
140        || open <= 0.0
141    {
142        return None;
143    }
144    let half = (width + 8.0) * open.clamp(0.0, 1.0) / 2.0;
145    let center = start + width / 2.0;
146    let left = (center - half).clamp(0.0, total);
147    let right = (center + half).clamp(0.0, total);
148    if right > left {
149        Some((left, right))
150    } else {
151        None
152    }
153}
154#[cfg(test)]
155mod tests {
156    use super::*;
157    #[test]
158    fn notch_matches_label_width_with_native_4dp_horizontal_padding() {
159        assert_eq!(Some((12.0, 68.0)), notch_range(16.0, 48.0, 1.0, 280.0));
160        assert_eq!(Some((26.0, 54.0)), notch_range(16.0, 48.0, 0.5, 280.0));
161        assert_eq!(None, notch_range(16.0, 48.0, 0.0, 280.0));
162    }
163    #[test]
164    fn notch_cannot_escape_field_or_poison_geometry() {
165        assert_eq!(Some((0.0, 40.0)), notch_range(-20.0, 100.0, 1.0, 40.0));
166        assert_eq!(None, notch_range(16.0, f64::NAN, 1.0, 280.0));
167        assert_eq!(None, notch_range(16.0, 0.0, 1.0, 280.0));
168    }
169}