Skip to main content

fui/controls/internal/
dropdown_field_presenter.rs

1use crate::controls::{DropdownColors, DropdownSizing};
2use crate::ffi::{AlignItems, FlexDirection, JustifyContent, TextVerticalAlign, Unit};
3use crate::node::{flex_box, text, FlexBox, TextNode};
4use crate::theme::Theme;
5use std::rc::Rc;
6
7const DEFAULT_CHEVRON_BOX_SIZE: f32 = 16.0;
8const DEFAULT_FIELD_PADDING_X: f32 = 16.0;
9const DEFAULT_FIELD_FONT_SIZE: f32 = 16.0;
10const DEFAULT_FIELD_HEIGHT: f32 = 32.0;
11
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct DropdownFieldMetrics {
14    pub height: f32,
15    pub font_size: f32,
16    pub chevron_box_size: f32,
17    pub padding_left: f32,
18    pub padding_top: f32,
19    pub padding_right: f32,
20    pub padding_bottom: f32,
21}
22
23impl DropdownFieldMetrics {
24    pub const fn new(
25        height: f32,
26        font_size: f32,
27        chevron_box_size: f32,
28        padding_left: f32,
29        padding_top: f32,
30        padding_right: f32,
31        padding_bottom: f32,
32    ) -> Self {
33        Self {
34            height,
35            font_size,
36            chevron_box_size,
37            padding_left,
38            padding_top,
39            padding_right,
40            padding_bottom,
41        }
42    }
43}
44
45pub const DEFAULT_DROPDOWN_FIELD_METRICS: DropdownFieldMetrics = DropdownFieldMetrics::new(
46    DEFAULT_FIELD_HEIGHT,
47    DEFAULT_FIELD_FONT_SIZE,
48    DEFAULT_CHEVRON_BOX_SIZE,
49    DEFAULT_FIELD_PADDING_X,
50    0.0,
51    DEFAULT_FIELD_PADDING_X,
52    0.0,
53);
54
55fn resolve_field_metrics(sizing: Option<DropdownSizing>) -> DropdownFieldMetrics {
56    let Some(sizing) = sizing else {
57        return DEFAULT_DROPDOWN_FIELD_METRICS;
58    };
59    if !sizing.has_field_height() && !sizing.has_field_font_size() && !sizing.has_chevron_box_size()
60    {
61        return DEFAULT_DROPDOWN_FIELD_METRICS;
62    }
63    let font_size = if sizing.has_field_font_size() {
64        sizing.field_font_size_px()
65    } else {
66        DEFAULT_DROPDOWN_FIELD_METRICS.font_size
67    };
68    let chevron_box_size = if sizing.has_chevron_box_size() {
69        sizing.chevron_box_size_px()
70    } else {
71        DEFAULT_DROPDOWN_FIELD_METRICS.chevron_box_size
72    };
73    let content_height = font_size.max(chevron_box_size);
74    let height = if sizing.has_field_height() {
75        sizing.field_height_px()
76    } else {
77        DEFAULT_DROPDOWN_FIELD_METRICS.height.max(content_height)
78    };
79    DropdownFieldMetrics::new(
80        height,
81        font_size,
82        chevron_box_size,
83        DEFAULT_FIELD_PADDING_X,
84        0.0,
85        DEFAULT_FIELD_PADDING_X,
86        0.0,
87    )
88}
89
90#[derive(Clone, Debug, PartialEq, Eq)]
91pub struct DropdownFieldVisualState {
92    pub open: bool,
93    pub focused: bool,
94    pub enabled: bool,
95    pub pressed: bool,
96    pub selected_label: String,
97}
98
99impl DropdownFieldVisualState {
100    pub fn new(
101        open: bool,
102        focused: bool,
103        enabled: bool,
104        pressed: bool,
105        selected_label: impl Into<String>,
106    ) -> Self {
107        Self {
108            open,
109            focused,
110            enabled,
111            pressed,
112            selected_label: selected_label.into(),
113        }
114    }
115}
116
117pub trait DropdownFieldPresenter {
118    fn root(&self) -> FlexBox;
119    fn value_host(&self) -> FlexBox;
120    fn value_node(&self) -> TextNode;
121    fn chevron_host(&self) -> FlexBox;
122    fn metrics(&self) -> DropdownFieldMetrics;
123    fn apply(&self, theme: Theme, state: &DropdownFieldVisualState, colors: Option<DropdownColors>);
124}
125
126pub trait DropdownFieldTemplate {
127    fn create(&self, sizing: Option<DropdownSizing>) -> Rc<dyn DropdownFieldPresenter>;
128}
129
130#[derive(Clone)]
131pub struct DefaultDropdownFieldPresenter {
132    root: FlexBox,
133    value_host: FlexBox,
134    value_node: TextNode,
135    chevron_host: FlexBox,
136    metrics: DropdownFieldMetrics,
137}
138
139impl DefaultDropdownFieldPresenter {
140    pub fn new(metrics: DropdownFieldMetrics) -> Self {
141        let value_node = text("");
142        value_node
143            .selectable(false)
144            .fill_size()
145            .text_limits(0, 1)
146            .wrapping(false);
147        value_node
148            .text_overflow_fade(true, false)
149            .text_vertical_align(TextVerticalAlign::Center);
150        let value_host = flex_box();
151        value_host.fill_size().child(&value_node);
152        let chevron_host = flex_box();
153        chevron_host
154            .width(metrics.chevron_box_size, Unit::Pixel)
155            .height(metrics.chevron_box_size, Unit::Pixel)
156            .align_items(AlignItems::Center)
157            .justify_content(JustifyContent::Center);
158        let root = flex_box();
159        root.flex_direction(FlexDirection::Row)
160            .align_items(AlignItems::Center)
161            .child(&value_host)
162            .child(&chevron_host);
163        Self {
164            root,
165            value_host,
166            value_node: value_node.clone(),
167            chevron_host,
168            metrics,
169        }
170    }
171}
172
173impl DropdownFieldPresenter for DefaultDropdownFieldPresenter {
174    fn root(&self) -> FlexBox {
175        self.root.clone()
176    }
177
178    fn value_host(&self) -> FlexBox {
179        self.value_host.clone()
180    }
181
182    fn value_node(&self) -> TextNode {
183        self.value_node.clone()
184    }
185
186    fn chevron_host(&self) -> FlexBox {
187        self.chevron_host.clone()
188    }
189
190    fn metrics(&self) -> DropdownFieldMetrics {
191        self.metrics
192    }
193
194    fn apply(
195        &self,
196        theme: Theme,
197        state: &DropdownFieldVisualState,
198        colors: Option<DropdownColors>,
199    ) {
200        let metrics = self.metrics;
201        let content_height = metrics
202            .font_size
203            .max(metrics.height - metrics.padding_top - metrics.padding_bottom);
204        let bg = if colors.is_some_and(|colors| colors.has_background()) {
205            colors.unwrap().background_color()
206        } else if state.pressed && state.enabled {
207            theme.colors.background
208        } else {
209            theme.colors.surface
210        };
211        let border_color = colors
212            .filter(|colors| colors.has_border())
213            .map(|colors| colors.border_color())
214            .unwrap_or(theme.colors.border);
215        self.root
216            .flex_direction(FlexDirection::Row)
217            .align_items(AlignItems::Center)
218            .height(metrics.height, Unit::Pixel)
219            .corner_radius(theme.spacing.sm)
220            .border(2.0, border_color)
221            .padding(
222                metrics.padding_left,
223                metrics.padding_top,
224                metrics.padding_right,
225                metrics.padding_bottom,
226            )
227            .bg_color(bg);
228        self.value_host.fill_size();
229        let text_color = if !state.enabled {
230            theme.colors.text_muted
231        } else {
232            colors
233                .filter(|colors| colors.has_text_primary())
234                .map(|colors| colors.text_primary_color())
235                .unwrap_or(theme.colors.text_primary)
236        };
237        self.value_node
238            .font_family(theme.fonts.body_family.clone())
239            .font_size(metrics.font_size)
240            .line_height(content_height)
241            .text_color(text_color);
242        self.chevron_host
243            .width(metrics.chevron_box_size, Unit::Pixel)
244            .height(metrics.chevron_box_size, Unit::Pixel)
245            .align_items(AlignItems::Center)
246            .justify_content(JustifyContent::Center);
247    }
248}
249
250#[derive(Clone, Copy, Debug, Default)]
251pub struct DefaultDropdownFieldTemplate;
252
253impl DropdownFieldTemplate for DefaultDropdownFieldTemplate {
254    fn create(&self, sizing: Option<DropdownSizing>) -> Rc<dyn DropdownFieldPresenter> {
255        create_default_dropdown_field_presenter(sizing)
256    }
257}
258
259pub const DEFAULT_DROPDOWN_FIELD_TEMPLATE: DefaultDropdownFieldTemplate =
260    DefaultDropdownFieldTemplate;
261
262pub fn create_default_dropdown_field_presenter(
263    sizing: Option<DropdownSizing>,
264) -> Rc<dyn DropdownFieldPresenter> {
265    Rc::new(DefaultDropdownFieldPresenter::new(resolve_field_metrics(
266        sizing,
267    )))
268}