Skip to main content

eui_core/
segmented.rs

1//! VAPP-162 (docs/round-5-contract.md §3): Segmented `orientation:
2//! vertical` and `option.count`. A vertical `bar` is THE navigation rail: a
3//! column of fixed items (item = the rail's width, 72 in every built-in
4//! theme, × `$control.tabBar`), laid out by the core (`layout_tree`). A
5//! `bar` item's `count` is a badge at its icon's top-end corner.
6
7use serde_json::Value;
8
9use crate::types::Props;
10
11fn str_prop<'a>(props: &'a Props, key: &str) -> Option<&'a str> {
12    props.get(key).and_then(Value::as_str)
13}
14
15/// Whether the Segmented stacks its segments top to bottom.
16pub fn is_vertical(props: &Props) -> bool {
17    str_prop(props, "orientation") == Some("vertical")
18}
19
20/// Whether the Segmented is the navigation rail (a vertical `bar`).
21pub fn is_rail(props: &Props) -> bool {
22    is_vertical(props) && str_prop(props, "variant") == Some("bar")
23}
24
25/// The object options a Segmented paints.
26pub fn item_count(props: &Props) -> usize {
27    props.get("items").and_then(Value::as_array).map_or(0, |a| a.iter().filter(|o| o.is_object()).count())
28}
29
30/// The rail's height: one `tab_bar` item per option.
31pub fn rail_height(props: &Props, tab_bar: f32) -> f32 {
32    item_count(props) as f32 * tab_bar
33}
34
35/// The item box of a rail at `index` (x, y, w, h) inside a rail `width` wide.
36pub fn rail_item(index: usize, width: f32, tab_bar: f32) -> (f32, f32, f32, f32) {
37    (0.0, index as f32 * tab_bar, width, tab_bar)
38}
39
40/// The badge text of an option's `count` (`99+` above 99); `None` when
41/// unset, not a positive number, or 0 (hidden). Only `bar` items paint it.
42pub fn badge_text(count: Option<&Value>) -> Option<String> {
43    let n = count.and_then(Value::as_f64).filter(|n| n.is_finite())?.floor();
44    if n <= 0.0 {
45        None
46    } else if n > 99.0 {
47        Some("99+".into())
48    } else {
49        Some(format!("{n}"))
50    }
51}
52
53/// The badge of an option on a Segmented of `variant` (only `bar` paints one).
54pub fn option_badge(variant: Option<&str>, option: &Value) -> Option<String> {
55    if variant != Some("bar") {
56        return None;
57    }
58    badge_text(option.get("count"))
59}
60
61/// The accessibility name of an item: the label, then the count
62/// ("Chats, 3"), when the item shows a badge.
63pub fn item_accessibility_label(label: &str, badge: Option<&str>) -> String {
64    match badge {
65        Some(b) => format!("{label}, {b}"),
66        None => label.to_string(),
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use serde_json::json;
74
75    #[test]
76    fn badges_hide_at_zero_and_cap_at_99() {
77        assert_eq!(badge_text(None), None);
78        assert_eq!(badge_text(Some(&json!(0))), None);
79        assert_eq!(badge_text(Some(&json!(3))), Some("3".into()));
80        assert_eq!(badge_text(Some(&json!(99))), Some("99".into()));
81        assert_eq!(badge_text(Some(&json!(100))), Some("99+".into()));
82        assert_eq!(badge_text(Some(&json!("3"))), None);
83        assert_eq!(option_badge(Some("segmented"), &json!({"count": 3})), None);
84        assert_eq!(option_badge(Some("bar"), &json!({"count": 3})).as_deref(), Some("3"));
85        assert_eq!(item_accessibility_label("Chats", Some("3")), "Chats, 3");
86    }
87}