1use 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 <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 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}