1use std::collections::HashMap;
12
13use serde::{Deserialize, Serialize};
14use serde_json::{Map, Value};
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct TextStyle {
19 pub font_size: f32,
20 pub font_weight: u16,
21 pub line_height: f32,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub font_family: Option<String>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub font_style: Option<String>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub text_transform: Option<String>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub letter_spacing: Option<f32>,
32}
33
34impl Default for TextStyle {
35 fn default() -> Self {
36 TextStyle { font_size: 14.0, font_weight: 400, line_height: 20.0, font_family: None, font_style: None, text_transform: None, letter_spacing: None }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
46pub struct ControlBox {
47 pub padding_horizontal: f32,
48 pub padding_vertical: f32,
49 #[serde(default)]
51 pub padding: [f32; 4],
52 pub border_width: f32,
53 pub gap: f32,
54 pub min_width: Option<f32>,
55 pub min_height: Option<f32>,
56 pub width: Option<f32>,
57 pub height: Option<f32>,
58}
59
60#[derive(Debug, Clone)]
63pub struct LeafRequest<'a> {
64 pub index: u32,
65 pub id: &'a str,
66 pub component: &'a str,
68 pub part: Option<&'a str>,
70 pub owner_component: Option<&'a str>,
73 pub props: &'a Map<String, Value>,
74 pub text_style: &'a TextStyle,
75 pub control: ControlBox,
76 pub lines: Option<u32>,
77}
78
79impl LeafRequest<'_> {
80 pub fn text(&self) -> &str {
82 self.props
83 .get("text")
84 .or_else(|| self.props.get("label"))
85 .or_else(|| self.props.get("title"))
86 .or_else(|| self.props.get("content"))
87 .or_else(|| self.props.get("placeholder"))
88 .and_then(Value::as_str)
89 .unwrap_or("")
90 }
91
92 pub fn display_text(&self) -> std::borrow::Cow<'_, str> {
97 let v = ["text", "label", "title", "content", "placeholder"].iter().find_map(|k| self.props.get(*k));
98 match v {
99 Some(Value::String(s)) => std::borrow::Cow::Borrowed(s.as_str()),
100 Some(v @ (Value::Number(_) | Value::Bool(_))) => std::borrow::Cow::Owned(crate::format::display_string(v)),
101 _ => std::borrow::Cow::Borrowed(""),
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
114pub struct Intrinsics {
115 pub min_content_width: f32,
116 pub max_content_width: f32,
117 pub height_at_max_content: f32,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub baseline: Option<f32>,
120}
121
122impl Intrinsics {
123 pub fn sanitised(self) -> Intrinsics {
129 let min = finite_or_zero(self.min_content_width);
130 let max = finite_or_zero(self.max_content_width);
131 Intrinsics {
132 min_content_width: min.min(max),
133 max_content_width: max.max(min),
134 height_at_max_content: finite_or_zero(self.height_at_max_content),
135 baseline: self.baseline.filter(|b| b.is_finite()),
136 }
137 }
138}
139
140pub fn finite_or_zero(v: f32) -> f32 {
143 if v.is_finite() && v > 0.0 {
144 v
145 } else {
146 0.0
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
152pub struct HeightRequest {
153 pub index: u32,
154 pub width: f32,
155}
156
157pub trait Measure {
160 fn measure_id(&self) -> u64 {
163 2
164 }
165 fn measure_intrinsics(&mut self, leaves: &[LeafRequest]) -> Vec<Intrinsics>;
166 fn measure_heights(&mut self, leaves: &[LeafRequest], requests: &[HeightRequest]) -> Vec<f32>;
167}
168
169pub struct DirectMeasure<F>(pub F, pub u64);
174
175pub struct DirectMeasureWithBaseline<F>(pub F, pub u64);
178
179impl<F: FnMut(&LeafRequest, Option<f32>) -> (f32, f32, Option<f32>)> Measure for DirectMeasureWithBaseline<F> {
180 fn measure_id(&self) -> u64 {
181 self.1
182 }
183
184 fn measure_intrinsics(&mut self, leaves: &[LeafRequest]) -> Vec<Intrinsics> {
185 leaves
186 .iter()
187 .map(|leaf| {
188 let (max_w, h, baseline) = (self.0)(leaf, None);
189 let (min_w, _, _) = (self.0)(leaf, Some(0.0));
190 Intrinsics { min_content_width: min_w, max_content_width: max_w, height_at_max_content: h, baseline }
191 })
192 .collect()
193 }
194
195 fn measure_heights(&mut self, leaves: &[LeafRequest], requests: &[HeightRequest]) -> Vec<f32> {
196 requests.iter().map(|r| leaves.iter().find(|l| l.index == r.index).map(|leaf| (self.0)(leaf, Some(r.width)).1).unwrap_or(0.0)).collect()
197 }
198}
199
200impl<F: FnMut(&LeafRequest, Option<f32>) -> (f32, f32)> Measure for DirectMeasure<F> {
201 fn measure_id(&self) -> u64 {
202 self.1
203 }
204
205 fn measure_intrinsics(&mut self, leaves: &[LeafRequest]) -> Vec<Intrinsics> {
206 leaves
207 .iter()
208 .map(|leaf| {
209 let (max_w, h) = (self.0)(leaf, None);
210 let (min_w, _) = (self.0)(leaf, Some(0.0));
211 Intrinsics { min_content_width: min_w, max_content_width: max_w, height_at_max_content: h, baseline: None }
212 })
213 .collect()
214 }
215
216 fn measure_heights(&mut self, leaves: &[LeafRequest], requests: &[HeightRequest]) -> Vec<f32> {
217 requests
218 .iter()
219 .map(|r| leaves.iter().find(|l| l.index == r.index).map(|leaf| (self.0)(leaf, Some(r.width)).1).unwrap_or(0.0))
220 .collect()
221 }
222}
223
224pub const FIXED_CHAR_WIDTH: f32 = 8.0;
225pub const FIXED_LINE_HEIGHT: f32 = 20.0;
226pub const FIXED_ASCENT: f32 = 15.0;
228
229#[derive(Debug, Clone, Default)]
233pub struct FixedMeasure {
234 pub sizes: HashMap<String, (f32, f32)>,
235 pub wrap: bool,
238}
239
240impl FixedMeasure {
241 pub fn with_sizes(sizes: HashMap<String, (f32, f32)>) -> FixedMeasure {
242 FixedMeasure { sizes, wrap: false }
243 }
244
245 fn chars(s: &str) -> f32 {
246 s.chars().count() as f32
247 }
248
249 pub fn intrinsic(&self, leaf: &LeafRequest) -> (f32, f32) {
251 if let Some(size) = self.sizes.get(leaf.id) {
252 return *size;
253 }
254 let text = leaf.text();
255 let str_prop = |k: &str| leaf.props.get(k).and_then(Value::as_str).unwrap_or("");
256 let c = leaf.control;
257 let (w, h) = match leaf.component {
258 _ if matches!(leaf.part, Some("input") | Some("search")) => {
261 let shown = Self::chars(text).max(Self::chars(str_prop("placeholder")));
262 (FIXED_CHAR_WIDTH * shown, c.height.unwrap_or(FIXED_LINE_HEIGHT))
263 }
264 "Text" | "Link" if text.contains('\n') => {
265 let lines = text.split('\n').count() as f32;
266 let widest = text.split('\n').map(Self::chars).fold(0.0, f32::max);
267 (FIXED_CHAR_WIDTH * widest, leaf.text_style.line_height.max(FIXED_LINE_HEIGHT) * lines)
268 }
269 "Text" | "Link" => (FIXED_CHAR_WIDTH * Self::chars(text), leaf.text_style.line_height.max(FIXED_LINE_HEIGHT)),
270 "Markdown" => {
271 let lines = text.lines().count().max(1) as f32;
272 let widest = text.lines().map(Self::chars).fold(0.0, f32::max);
273 (FIXED_CHAR_WIDTH * widest, FIXED_LINE_HEIGHT * lines)
274 }
275 "Button" if leaf.props.get("loading").and_then(Value::as_bool) == Some(true) => {
278 let chars = Self::chars(str_prop("label")).max(Self::chars(str_prop("pendingLabel")));
279 (FIXED_CHAR_WIDTH * chars + 24.0, c.height.unwrap_or(36.0))
280 }
281 "Button" | "Toggle" => (FIXED_CHAR_WIDTH * Self::chars(str_prop("label")) + 24.0, c.height.unwrap_or(36.0)),
282 "SearchList" if leaf.part.is_none() => (240.0, 40.0 + FIXED_LINE_HEIGHT * leaf.props.get("items").and_then(Value::as_array).map_or(0, Vec::len).min(8) as f32),
283 "Input" | "Select" | "DatePicker" => (160.0, c.height.unwrap_or(36.0)),
284 "Textarea" if leaf.part.is_none() || leaf.part == Some("field") => (160.0, c.height.unwrap_or_else(|| Self::textarea_height(leaf))),
285 "Textarea" | "Composer" => (160.0, c.height.unwrap_or(72.0)),
286 "NumberField" | "TimePicker" | "DateRangePicker" | "ChipInput" if leaf.part.is_none() => (160.0, c.height.unwrap_or(36.0)),
287 "CodeBlock" if leaf.part.is_none() => {
288 let code = str_prop("code");
289 let lines = code.split('\n').count().max(1) as f32;
290 let widest = code.split('\n').map(Self::chars).fold(0.0, f32::max);
291 (FIXED_CHAR_WIDTH * widest, FIXED_LINE_HEIGHT * lines)
292 }
293 "Chart" => (320.0, leaf.props.get("height").and_then(Value::as_f64).unwrap_or(200.0) as f32),
294 "ChoiceTiles" if leaf.part.is_none() => {
298 let options = leaf.props.get("options").and_then(Value::as_array);
299 let n = options.map_or(0, Vec::len) as f32;
300 let cols = leaf.props.get("columns").and_then(Value::as_f64).unwrap_or(2.0).round().clamp(1.0, 4.0) as f32;
301 let rows = (n / cols).ceil();
302 let pictures = options.is_some_and(|o| o.iter().any(|o| o.get("image").and_then(Value::as_str).is_some_and(|s| !s.is_empty())));
303 let tile_w = (320.0 - 8.0 * (cols - 1.0)) / cols - 18.0;
304 let tile_h = if pictures { tile_w / 1.6 + 4.0 } else { 0.0 } + 2.0 * FIXED_LINE_HEIGHT + 4.0 + 18.0;
305 (320.0, (rows * tile_h + (rows - 1.0).max(0.0) * 8.0).max(0.0))
306 }
307 "Switch" => (44.0, 24.0),
308 "Checkbox" | "Radio" => (16.0, 16.0),
309 "Avatar" => {
310 let s = c.width.unwrap_or(32.0);
311 (s, s)
312 }
313 "Icon" => (c.width.unwrap_or(16.0), c.height.unwrap_or(16.0)),
314 "Image" | "Video" => (320.0, 180.0),
315 "Motion" => crate::layout_tree::motion_size(&leaf.props).map_or((0.0, 0.0), |(w, h)| (w as f32, h as f32)),
317 "Slider" => (160.0, 16.0),
318 "Spinner" | "Ring" => (c.width.unwrap_or(20.0), c.height.unwrap_or(20.0)),
319 "Skeleton" => (
320 leaf.props.get("width").and_then(Value::as_f64).unwrap_or(120.0) as f32,
321 leaf.props.get("height").and_then(Value::as_f64).unwrap_or(16.0) as f32,
322 ),
323 _ => (c.width.unwrap_or(0.0), c.height.unwrap_or(0.0)),
324 };
325 (c.width.unwrap_or(w).max(c.min_width.unwrap_or(0.0)), c.height.unwrap_or(h).max(c.min_height.unwrap_or(0.0)))
326 }
327
328 fn textarea_height(leaf: &LeafRequest) -> f32 {
331 let rows = leaf.props.get("rows").and_then(Value::as_f64).unwrap_or(3.0).max(1.0) as f32;
332 let mut lines = rows;
333 if leaf.props.get("autosize").and_then(Value::as_bool) == Some(true) {
334 let value = leaf.props.get("value").and_then(Value::as_str).unwrap_or("");
335 let typed = value.split('\n').count() as f32;
336 let max = leaf.props.get("maxRows").and_then(Value::as_f64).map(|m| m as f32).unwrap_or(f32::INFINITY);
337 lines = typed.max(rows).min(max.max(rows));
338 }
339 lines * FIXED_LINE_HEIGHT + leaf.control.padding[0] + leaf.control.padding[2]
340 }
341
342 pub fn baseline(&self, leaf: &LeafRequest, height: f32) -> Option<f32> {
345 match leaf.component {
346 "Text" | "Link" | "Markdown" | "CodeBlock" => Some(leaf.control.padding[0] + FIXED_ASCENT),
347 "Checkbox" | "Radio" | "Switch" | "Avatar" | "Icon" | "Image" | "Video" | "Slider" | "Spinner" | "Ring" | "Skeleton" | "Chart" | "Motion" => None,
348 _ if leaf.text().is_empty() => None,
349 _ => Some(((height - FIXED_LINE_HEIGHT) / 2.0).max(0.0) + FIXED_ASCENT),
350 }
351 }
352
353 fn wraps(&self, leaf: &LeafRequest) -> bool {
354 self.wrap && !self.sizes.contains_key(leaf.id) && matches!(leaf.component, "Text" | "Markdown")
355 }
356}
357
358impl Measure for FixedMeasure {
359 fn measure_id(&self) -> u64 {
360 1
361 }
362
363 fn measure_intrinsics(&mut self, leaves: &[LeafRequest]) -> Vec<Intrinsics> {
364 leaves
365 .iter()
366 .map(|leaf| {
367 let (w, h) = self.intrinsic(leaf);
368 let min = if self.wraps(leaf) {
369 leaf.text().split_whitespace().map(Self::chars).fold(0.0, f32::max) * FIXED_CHAR_WIDTH
370 } else {
371 w
372 };
373 let baseline = if self.sizes.contains_key(leaf.id) { None } else { self.baseline(leaf, h) };
374 Intrinsics { min_content_width: min, max_content_width: w, height_at_max_content: h, baseline }
375 })
376 .collect()
377 }
378
379 fn measure_heights(&mut self, leaves: &[LeafRequest], requests: &[HeightRequest]) -> Vec<f32> {
380 requests
381 .iter()
382 .map(|r| {
383 let Some(leaf) = leaves.iter().find(|l| l.index == r.index) else { return 0.0 };
384 let (w, h) = self.intrinsic(leaf);
385 if !self.wraps(leaf) || r.width >= w || r.width <= 0.0 {
386 return h;
387 }
388 let per_line = (r.width / FIXED_CHAR_WIDTH).floor().max(1.0);
389 let lines = (Self::chars(leaf.text()) / per_line).ceil().max(1.0);
390 let lines = match leaf.lines {
391 Some(n) if n > 0 => lines.min(n as f32),
392 _ => lines,
393 };
394 lines * FIXED_LINE_HEIGHT
395 })
396 .collect()
397 }
398}
399
400#[derive(Debug, Default, Clone)]
403pub struct MeasureMemo {
404 pub intrinsics: HashMap<u32, Intrinsics>,
405 heights: HashMap<u32, Vec<(i64, f32)>>,
409}
410
411impl MeasureMemo {
412 pub fn width_key(width: f32) -> i64 {
413 (width * 64.0).round() as i64
414 }
415
416 pub fn clear(&mut self) {
417 self.intrinsics.clear();
418 self.heights.clear();
419 }
420
421 pub fn forget(&mut self, index: u32) {
422 self.intrinsics.remove(&index);
423 self.heights.remove(&index);
424 }
425
426 pub fn height_at(&self, index: u32, width: f32) -> Option<f32> {
427 let key = Self::width_key(width);
428 self.heights.get(&index)?.iter().find(|(w, _)| *w == key).map(|(_, h)| *h)
429 }
430
431 pub fn insert_height(&mut self, index: u32, width_key: i64, height: f32) {
433 let at = self.heights.entry(index).or_default();
434 match at.iter_mut().find(|(w, _)| *w == width_key) {
435 Some(entry) => entry.1 = height,
436 None => at.push((width_key, height)),
437 }
438 }
439
440 pub fn heights_of(&self, index: u32) -> &[(i64, f32)] {
442 self.heights.get(&index).map(Vec::as_slice).unwrap_or(&[])
443 }
444}
445
446#[derive(Debug, Clone, Copy)]
450pub struct MeasureRequest {
451 pub index: u32,
452 pub known_width: Option<f32>,
453 pub known_height: Option<f32>,
454 pub available_width: taffy::AvailableSpace,
455 pub available_height: taffy::AvailableSpace,
456}
457
458impl MeasureMemo {
459 pub fn answer(&self, req: &MeasureRequest) -> (taffy::geometry::Size<f32>, bool, Option<f32>) {
464 let Some(i) = self.intrinsics.get(&req.index) else {
465 return (taffy::geometry::Size { width: req.known_width.unwrap_or(0.0), height: req.known_height.unwrap_or(0.0) }, false, None);
466 };
467 let width = req.known_width.unwrap_or(match req.available_width {
468 taffy::AvailableSpace::Definite(w) => w.clamp(i.min_content_width.min(i.max_content_width), i.max_content_width),
471 taffy::AvailableSpace::MinContent => i.min_content_width,
472 taffy::AvailableSpace::MaxContent => i.max_content_width,
473 });
474 if let Some(h) = req.known_height {
475 return (taffy::geometry::Size { width, height: h }, false, i.baseline);
476 }
477 if width + 0.01 >= i.max_content_width || (i.max_content_width - i.min_content_width).abs() < 0.01 {
480 return (taffy::geometry::Size { width, height: i.height_at_max_content }, false, i.baseline);
481 }
482 match self.height_at(req.index, width) {
483 Some(h) => (taffy::geometry::Size { width, height: h }, false, i.baseline),
484 None => (taffy::geometry::Size { width, height: i.height_at_max_content }, true, i.baseline),
485 }
486 }
487}