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}