Skip to main content

Row

Struct Row 

Source
pub struct Row {
    pub id: MenuId,
    pub segments: Vec<Segment>,
    pub leading: Option<Icon>,
    pub trailing: Option<Icon>,
    pub enabled: bool,
    pub checked: Option<bool>,
    pub background: Option<Color>,
    pub min_height: Option<f32>,
    pub accessibility_label: Option<String>,
}
Expand description

One menu row. A row is interactive (carries a MenuId) unless it is used as an Item::SectionHeader.

Fields§

§id: MenuId

The click id. MenuId::none marks a non-interactive row.

§segments: Vec<Segment>

Left→right segments (multi-column layout).

§leading: Option<Icon>

Optional leading icon column (logo, avatar, checkmark).

§trailing: Option<Icon>

Optional trailing icon column.

§enabled: bool

Whether the row is clickable. Disabled rows are dimmed and inert.

§checked: Option<bool>

Some(true/false) shows a check column; None reserves no check column.

§background: Option<Color>

Optional explicit row background (else theme hover/selection handling).

§min_height: Option<f32>

Optional minimum row height in logical points (else the theme default).

§accessibility_label: Option<String>

Optional override for the accessible name announced by screen readers, used in place of Row::accessible_name. Needed for icon-only rows (no segments), whose derived name would otherwise be empty and silent to an assistive technology (spec 30 §1.4).

Implementations§

Source§

impl Row

Source

pub fn new(id: impl Into<MenuId>) -> Self

A new enabled row with the given click id and no segments.

Examples found in repository?
examples/demo_tray.rs (line 55)
39fn account_submenu(slug: &str) -> Menu {
40    Menu::new()
41        .row(Row::info().segments(vec![
42            Segment::new("Session resets in").flex(Flex::Grow),
43            Segment::new("3h 12m")
44                .align(Align::Right)
45                .color(Color::SecondaryLabel),
46        ]))
47        .row(Row::info().segments(vec![
48            Segment::new("Weekly resets in").flex(Flex::Grow),
49            Segment::new("2d 4h")
50                .align(Align::Right)
51                .color(Color::SecondaryLabel),
52        ]))
53        .row(Row::info().segment(Segment::new("updated 1m ago").color(Color::SecondaryLabel)))
54        .separator()
55        .row(Row::new(format!("switch:{slug}")).label("Switch to this account"))
56        .row(Row::new(format!("launch:{slug}")).label("Launch client"))
57        .row(Row::new(format!("remove:{slug}")).label("Remove…"))
58}
59
60fn demo_menu() -> Menu {
61    let claude_logo = swatch_png(217, 119, 87);
62    let codex_logo = swatch_png(80, 80, 90);
63
64    Menu::new()
65        .section_header(
66            Row::info()
67                .leading(Icon::from_png_bytes(claude_logo))
68                .segment(Segment::new("Claude").font(Font::system(13.0, Weight::Bold))),
69        )
70        .submenu(
71            Row::new("acct:claude:me")
72                .leading(Icon::Checkmark)
73                .checked(true)
74                .segments(vec![
75                    Segment::new("me@example.com")
76                        .flex(Flex::Grow)
77                        .font(Font::system(13.0, Weight::Bold)),
78                    Segment::new("47% / 89%")
79                        .align(Align::Right)
80                        .runs(vec![StyleRun::new(6, 3, Color::SystemRed)]),
81                ]),
82            account_submenu("claude:me"),
83        )
84        .submenu(
85            Row::new("acct:claude:work").segments(vec![
86                Segment::new("work@example.com").flex(Flex::Grow),
87                Segment::new("12% / 30%")
88                    .align(Align::Right)
89                    .runs(vec![StyleRun::new(0, 3, Color::SystemGreen)]),
90            ]),
91            account_submenu("claude:work"),
92        )
93        .separator()
94        .section_header(
95            Row::info()
96                .leading(Icon::from_png_bytes(codex_logo))
97                .segment(Segment::new("Codex").font(Font::system(13.0, Weight::Bold))),
98        )
99        .row(Row::new("switch:codex:me").segments(vec![
100            Segment::new("me@example.com").flex(Flex::Grow),
101            Segment::new("3h 12m")
102                .align(Align::Right)
103                .runs(vec![StyleRun::new(0, 6, Color::SystemOrange)]),
104        ]))
105        .separator()
106        .submenu(
107            Row::new("settings").label("Settings"),
108            Menu::new()
109                .row(Row::new("settings:apikey").label("API key…"))
110                .row(Row::new("settings:autoswap").label("Auto-swap accounts"))
111                .separator()
112                .row(Row::new("settings:notifications").label("Notifications…")),
113        )
114        .row(Row::new("quit").segments(vec![
115            Segment::new("Quit").flex(Flex::Grow),
116            Segment::new(concat!("usagio v", env!("CARGO_PKG_VERSION")))
117                .align(Align::Right)
118                .color(Color::SecondaryLabel),
119        ]))
120}
More examples
Hide additional examples
examples/usagio_menu.rs (line 66)
56fn account_submenu(acct: &Account) -> Menu {
57    let mut menu = Menu::new()
58        // Info rows read as normal text but are non-interactive (id = none).
59        .row(Row::info().label("Session resets in 2h 41m"))
60        .row(Row::info().label("Weekly resets in 3d 4h"))
61        .row(Row::info().segment(Segment::new("updated 12s ago").color(Color::SecondaryLabel)))
62        .separator();
63
64    if acct.active {
65        menu = menu.row(
66            Row::new("noop")
67                .leading(Icon::Checkmark)
68                .segment(Segment::new("Active").color(Color::SystemGreen))
69                .enabled(false),
70        );
71    } else {
72        menu = menu.row(
73            Row::new(format!("switch:{}:{}", acct.provider, acct.key))
74                .label("Switch to this account"),
75        );
76    }
77    if acct.supports_launch {
78        menu = menu.row(
79            Row::new(format!("launch:{}:{}", acct.provider, acct.key))
80                .label("Launch client")
81                // A named symbol trails the action (SF Symbol on macOS).
82                .trailing(Icon::Symbol("arrow.up.forward.app")),
83        );
84    }
85    if acct.supports_remove {
86        menu = menu.row(
87            Row::new(format!("remove:{}:{}", acct.provider, acct.key)).label("Remove\u{2026}"),
88        );
89    }
90    menu
91}
92
93/// The Capture submenu: one entry per provider to capture the current login.
94fn capture_submenu(groups: &[Group]) -> Menu {
95    let mut menu = Menu::new();
96    for group in groups {
97        menu = menu.row(
98            Row::new(format!("capture:{}", group.display_name.to_lowercase()))
99                .leading(Icon::from_png_bytes(group.icon_png))
100                .label(format!("Capture {} login", group.display_name)),
101        );
102    }
103    menu
104}
105
106/// The Settings submenu: a checkable toggle, an auto-swap threshold flyout, and
107/// a backup flyout — exercising nested submenus, `checked`, and an SVG icon.
108fn settings_submenu() -> Menu {
109    let autoswap = Menu::new()
110        .row(Row::new("autoswap:off").checked(false).label("Off"))
111        .row(Row::new("autoswap:80").checked(false).label("At 80%"))
112        .row(Row::new("autoswap:90").checked(true).label("At 90%"))
113        .separator()
114        .row(Row::new("autoswap:now").label("Swap now"));
115
116    let backup = Menu::new()
117        .row(Row::new("backup:save").label("Save backup\u{2026}"))
118        .row(Row::new("backup:restore").label("Restore backup\u{2026}"));
119
120    Menu::new()
121        .row(
122            Row::new("notifications:limits")
123                .checked(true)
124                .label("Notify near limits"),
125        )
126        .submenu(Row::new("autoswap").label("Auto-swap threshold"), autoswap)
127        .submenu(
128            Row::new("backup")
129                // An SVG leading icon, rasterized per-DPI at draw time.
130                .leading(Icon::from_svg_bytes(
131                    br#"<svg xmlns="http://www.w3.org/2000/svg"/>"#.as_slice(),
132                ))
133                .label("Backup"),
134            backup,
135        )
136        .separator()
137        .row(Row::new("refresh:now").label("Refresh now"))
138}
139
140fn build(groups: &[Group]) -> Menu {
141    let mut menu = Menu::new();
142
143    for group in groups {
144        // Bold, logo'd, non-interactive group header (Claude, then Codex).
145        menu = menu.section_header(
146            Row::info()
147                .leading(Icon::from_png_bytes(group.icon_png))
148                .segment(Segment::new(group.display_name).font(Font::system(13.0, Weight::Bold))),
149        );
150
151        for acct in &group.accounts {
152            // The label Grows to eat the leftover width, so the value segment
153            // sits flush at the true right edge — no reserved chevron column.
154            let label = Segment::new(acct.display)
155                .flex(Flex::Grow)
156                .font(if acct.active {
157                    Font::system(13.0, Weight::Bold)
158                } else {
159                    Font::system(13.0, Weight::Regular)
160                });
161
162            let value = Segment::new(acct.trailing)
163                .align(Align::Right)
164                .runs(severity_runs(&acct.severity));
165
166            let mut label_row = Row::new(format!("switch:{}:{}", acct.provider, acct.key))
167                .segments(vec![label, value])
168                .checked(acct.active);
169            if acct.active {
170                label_row = label_row.leading(Icon::Checkmark);
171            }
172
173            menu = menu.submenu(label_row, account_submenu(acct));
174        }
175
176        menu = menu.separator();
177    }
178
179    // Bottom actions — now with populated submenus.
180    menu = menu
181        .submenu(
182            Row::new("capture").label("Capture current login"),
183            capture_submenu(groups),
184        )
185        .submenu(Row::new("settings").label("Settings"), settings_submenu());
186
187    // "Quit ........ usagio vX" — label Grows, version tail is greyed + flush-right.
188    menu = menu.row(Row::new("quit").segments(vec![
189        Segment::new("Quit").flex(Flex::Grow),
190        Segment::new(format!("usagio v{}", env!("CARGO_PKG_VERSION")))
191            .align(Align::Right)
192            .color(Color::SecondaryLabel),
193    ]));
194
195    menu
196}
197
198fn icon_tag(icon: &Option<Icon>) -> &'static str {
199    match icon {
200        None => "",
201        Some(Icon::Checkmark) => " [check]",
202        Some(Icon::Png(_)) => " [png]",
203        Some(Icon::Svg(_)) => " [svg]",
204        Some(Icon::Symbol(_)) => " [symbol]",
205    }
206}
207
208fn print_row(row: &Row, prefix: &str, pad: &str) {
209    let check = match row.checked {
210        Some(true) => "[x] ",
211        Some(false) => "[ ] ",
212        None => "",
213    };
214    let dim = if row.enabled { "" } else { " (disabled)" };
215    println!(
216        "{pad}{prefix}{}{check}{}{}{}  [{}]",
217        icon_tag(&row.leading),
218        row_text(row),
219        icon_tag(&row.trailing),
220        dim,
221        row.id.as_str(),
222    );
223}
224
225fn print_menu(menu: &Menu, depth: usize) {
226    let pad = "  ".repeat(depth);
227    for item in &menu.items {
228        match item {
229            Item::Separator => println!("{pad}----"),
230            Item::SectionHeader(row) => {
231                println!("{pad}#{} {}", icon_tag(&row.leading), row_text(row))
232            }
233            Item::Row(row) => print_row(row, "- ", &pad),
234            Item::Submenu { label, menu } => {
235                print_row(label, "> ", &pad);
236                print_menu(menu, depth + 1);
237            }
238        }
239    }
240}
241
242fn row_text(row: &Row) -> String {
243    row.segments
244        .iter()
245        .map(|s| s.text.as_str())
246        .collect::<Vec<_>>()
247        .join("  ")
248}
249
250fn demo_theme_resolution() {
251    // A consumer can resolve semantic colors against any theme with no GUI.
252    let dark = Theme::dark();
253    let label = dark.resolve(Color::Label);
254    let red = dark.resolve(Color::SystemRed);
255    println!(
256        "theme resolution (dark): Label -> rgba({},{},{},{}), SystemRed -> rgba({},{},{},{})",
257        label.r, label.g, label.b, label.a, red.r, red.g, red.b, red.a,
258    );
259}
260
261fn demo_flush_right_layout() {
262    // The "Quit ...... usagio v1" row: a Grow label + a Fixed, right-aligned
263    // version tail. Given measured intrinsic widths, the layout engine flushes
264    // the tail to the right edge with no reserved column.
265    let content_width = 220.0;
266    let segs = [
267        SegmentMetrics::new(30.0, Flex::Grow, Align::Left), // "Quit"
268        SegmentMetrics::new(60.0, Flex::Fixed, Align::Right), // "usagio v1"
269    ];
270    let boxes = resolve_segments(&segs, content_width);
271    println!(
272        "flush-right layout: content {content_width}px -> tail text starts at x={} (right edge {})",
273        boxes[1].text_x,
274        content_width - 60.0,
275    );
276}
277
278fn demo_context_menu() {
279    // The pointer-anchored primitive that also works on Linux.
280    let menu = Menu::new()
281        .row(Row::new("copy").label("Copy"))
282        .row(Row::new("paste").label("Paste"))
283        .separator()
284        .row(Row::new("select-all").label("Select All"));
285    let cm = ContextMenu::new(menu).on_click(|id| println!("context click: {}", id.as_str()));
286    // Exercise the dispatch path without a GUI.
287    cm.dispatch(&"copy".into());
288    println!("context menu has {} items", cm.menu().len());
289}
Source

pub fn info() -> Self

A non-interactive row (id = MenuId::none); handy for section headers and pure info lines.

Examples found in repository?
examples/demo_tray.rs (line 41)
39fn account_submenu(slug: &str) -> Menu {
40    Menu::new()
41        .row(Row::info().segments(vec![
42            Segment::new("Session resets in").flex(Flex::Grow),
43            Segment::new("3h 12m")
44                .align(Align::Right)
45                .color(Color::SecondaryLabel),
46        ]))
47        .row(Row::info().segments(vec![
48            Segment::new("Weekly resets in").flex(Flex::Grow),
49            Segment::new("2d 4h")
50                .align(Align::Right)
51                .color(Color::SecondaryLabel),
52        ]))
53        .row(Row::info().segment(Segment::new("updated 1m ago").color(Color::SecondaryLabel)))
54        .separator()
55        .row(Row::new(format!("switch:{slug}")).label("Switch to this account"))
56        .row(Row::new(format!("launch:{slug}")).label("Launch client"))
57        .row(Row::new(format!("remove:{slug}")).label("Remove…"))
58}
59
60fn demo_menu() -> Menu {
61    let claude_logo = swatch_png(217, 119, 87);
62    let codex_logo = swatch_png(80, 80, 90);
63
64    Menu::new()
65        .section_header(
66            Row::info()
67                .leading(Icon::from_png_bytes(claude_logo))
68                .segment(Segment::new("Claude").font(Font::system(13.0, Weight::Bold))),
69        )
70        .submenu(
71            Row::new("acct:claude:me")
72                .leading(Icon::Checkmark)
73                .checked(true)
74                .segments(vec![
75                    Segment::new("me@example.com")
76                        .flex(Flex::Grow)
77                        .font(Font::system(13.0, Weight::Bold)),
78                    Segment::new("47% / 89%")
79                        .align(Align::Right)
80                        .runs(vec![StyleRun::new(6, 3, Color::SystemRed)]),
81                ]),
82            account_submenu("claude:me"),
83        )
84        .submenu(
85            Row::new("acct:claude:work").segments(vec![
86                Segment::new("work@example.com").flex(Flex::Grow),
87                Segment::new("12% / 30%")
88                    .align(Align::Right)
89                    .runs(vec![StyleRun::new(0, 3, Color::SystemGreen)]),
90            ]),
91            account_submenu("claude:work"),
92        )
93        .separator()
94        .section_header(
95            Row::info()
96                .leading(Icon::from_png_bytes(codex_logo))
97                .segment(Segment::new("Codex").font(Font::system(13.0, Weight::Bold))),
98        )
99        .row(Row::new("switch:codex:me").segments(vec![
100            Segment::new("me@example.com").flex(Flex::Grow),
101            Segment::new("3h 12m")
102                .align(Align::Right)
103                .runs(vec![StyleRun::new(0, 6, Color::SystemOrange)]),
104        ]))
105        .separator()
106        .submenu(
107            Row::new("settings").label("Settings"),
108            Menu::new()
109                .row(Row::new("settings:apikey").label("API key…"))
110                .row(Row::new("settings:autoswap").label("Auto-swap accounts"))
111                .separator()
112                .row(Row::new("settings:notifications").label("Notifications…")),
113        )
114        .row(Row::new("quit").segments(vec![
115            Segment::new("Quit").flex(Flex::Grow),
116            Segment::new(concat!("usagio v", env!("CARGO_PKG_VERSION")))
117                .align(Align::Right)
118                .color(Color::SecondaryLabel),
119        ]))
120}
More examples
Hide additional examples
examples/usagio_menu.rs (line 59)
56fn account_submenu(acct: &Account) -> Menu {
57    let mut menu = Menu::new()
58        // Info rows read as normal text but are non-interactive (id = none).
59        .row(Row::info().label("Session resets in 2h 41m"))
60        .row(Row::info().label("Weekly resets in 3d 4h"))
61        .row(Row::info().segment(Segment::new("updated 12s ago").color(Color::SecondaryLabel)))
62        .separator();
63
64    if acct.active {
65        menu = menu.row(
66            Row::new("noop")
67                .leading(Icon::Checkmark)
68                .segment(Segment::new("Active").color(Color::SystemGreen))
69                .enabled(false),
70        );
71    } else {
72        menu = menu.row(
73            Row::new(format!("switch:{}:{}", acct.provider, acct.key))
74                .label("Switch to this account"),
75        );
76    }
77    if acct.supports_launch {
78        menu = menu.row(
79            Row::new(format!("launch:{}:{}", acct.provider, acct.key))
80                .label("Launch client")
81                // A named symbol trails the action (SF Symbol on macOS).
82                .trailing(Icon::Symbol("arrow.up.forward.app")),
83        );
84    }
85    if acct.supports_remove {
86        menu = menu.row(
87            Row::new(format!("remove:{}:{}", acct.provider, acct.key)).label("Remove\u{2026}"),
88        );
89    }
90    menu
91}
92
93/// The Capture submenu: one entry per provider to capture the current login.
94fn capture_submenu(groups: &[Group]) -> Menu {
95    let mut menu = Menu::new();
96    for group in groups {
97        menu = menu.row(
98            Row::new(format!("capture:{}", group.display_name.to_lowercase()))
99                .leading(Icon::from_png_bytes(group.icon_png))
100                .label(format!("Capture {} login", group.display_name)),
101        );
102    }
103    menu
104}
105
106/// The Settings submenu: a checkable toggle, an auto-swap threshold flyout, and
107/// a backup flyout — exercising nested submenus, `checked`, and an SVG icon.
108fn settings_submenu() -> Menu {
109    let autoswap = Menu::new()
110        .row(Row::new("autoswap:off").checked(false).label("Off"))
111        .row(Row::new("autoswap:80").checked(false).label("At 80%"))
112        .row(Row::new("autoswap:90").checked(true).label("At 90%"))
113        .separator()
114        .row(Row::new("autoswap:now").label("Swap now"));
115
116    let backup = Menu::new()
117        .row(Row::new("backup:save").label("Save backup\u{2026}"))
118        .row(Row::new("backup:restore").label("Restore backup\u{2026}"));
119
120    Menu::new()
121        .row(
122            Row::new("notifications:limits")
123                .checked(true)
124                .label("Notify near limits"),
125        )
126        .submenu(Row::new("autoswap").label("Auto-swap threshold"), autoswap)
127        .submenu(
128            Row::new("backup")
129                // An SVG leading icon, rasterized per-DPI at draw time.
130                .leading(Icon::from_svg_bytes(
131                    br#"<svg xmlns="http://www.w3.org/2000/svg"/>"#.as_slice(),
132                ))
133                .label("Backup"),
134            backup,
135        )
136        .separator()
137        .row(Row::new("refresh:now").label("Refresh now"))
138}
139
140fn build(groups: &[Group]) -> Menu {
141    let mut menu = Menu::new();
142
143    for group in groups {
144        // Bold, logo'd, non-interactive group header (Claude, then Codex).
145        menu = menu.section_header(
146            Row::info()
147                .leading(Icon::from_png_bytes(group.icon_png))
148                .segment(Segment::new(group.display_name).font(Font::system(13.0, Weight::Bold))),
149        );
150
151        for acct in &group.accounts {
152            // The label Grows to eat the leftover width, so the value segment
153            // sits flush at the true right edge — no reserved chevron column.
154            let label = Segment::new(acct.display)
155                .flex(Flex::Grow)
156                .font(if acct.active {
157                    Font::system(13.0, Weight::Bold)
158                } else {
159                    Font::system(13.0, Weight::Regular)
160                });
161
162            let value = Segment::new(acct.trailing)
163                .align(Align::Right)
164                .runs(severity_runs(&acct.severity));
165
166            let mut label_row = Row::new(format!("switch:{}:{}", acct.provider, acct.key))
167                .segments(vec![label, value])
168                .checked(acct.active);
169            if acct.active {
170                label_row = label_row.leading(Icon::Checkmark);
171            }
172
173            menu = menu.submenu(label_row, account_submenu(acct));
174        }
175
176        menu = menu.separator();
177    }
178
179    // Bottom actions — now with populated submenus.
180    menu = menu
181        .submenu(
182            Row::new("capture").label("Capture current login"),
183            capture_submenu(groups),
184        )
185        .submenu(Row::new("settings").label("Settings"), settings_submenu());
186
187    // "Quit ........ usagio vX" — label Grows, version tail is greyed + flush-right.
188    menu = menu.row(Row::new("quit").segments(vec![
189        Segment::new("Quit").flex(Flex::Grow),
190        Segment::new(format!("usagio v{}", env!("CARGO_PKG_VERSION")))
191            .align(Align::Right)
192            .color(Color::SecondaryLabel),
193    ]));
194
195    menu
196}
Source

pub fn label(self, text: impl Into<String>) -> Self

Append a plain-text left-aligned segment (convenience).

Examples found in repository?
examples/demo_tray.rs (line 55)
39fn account_submenu(slug: &str) -> Menu {
40    Menu::new()
41        .row(Row::info().segments(vec![
42            Segment::new("Session resets in").flex(Flex::Grow),
43            Segment::new("3h 12m")
44                .align(Align::Right)
45                .color(Color::SecondaryLabel),
46        ]))
47        .row(Row::info().segments(vec![
48            Segment::new("Weekly resets in").flex(Flex::Grow),
49            Segment::new("2d 4h")
50                .align(Align::Right)
51                .color(Color::SecondaryLabel),
52        ]))
53        .row(Row::info().segment(Segment::new("updated 1m ago").color(Color::SecondaryLabel)))
54        .separator()
55        .row(Row::new(format!("switch:{slug}")).label("Switch to this account"))
56        .row(Row::new(format!("launch:{slug}")).label("Launch client"))
57        .row(Row::new(format!("remove:{slug}")).label("Remove…"))
58}
59
60fn demo_menu() -> Menu {
61    let claude_logo = swatch_png(217, 119, 87);
62    let codex_logo = swatch_png(80, 80, 90);
63
64    Menu::new()
65        .section_header(
66            Row::info()
67                .leading(Icon::from_png_bytes(claude_logo))
68                .segment(Segment::new("Claude").font(Font::system(13.0, Weight::Bold))),
69        )
70        .submenu(
71            Row::new("acct:claude:me")
72                .leading(Icon::Checkmark)
73                .checked(true)
74                .segments(vec![
75                    Segment::new("me@example.com")
76                        .flex(Flex::Grow)
77                        .font(Font::system(13.0, Weight::Bold)),
78                    Segment::new("47% / 89%")
79                        .align(Align::Right)
80                        .runs(vec![StyleRun::new(6, 3, Color::SystemRed)]),
81                ]),
82            account_submenu("claude:me"),
83        )
84        .submenu(
85            Row::new("acct:claude:work").segments(vec![
86                Segment::new("work@example.com").flex(Flex::Grow),
87                Segment::new("12% / 30%")
88                    .align(Align::Right)
89                    .runs(vec![StyleRun::new(0, 3, Color::SystemGreen)]),
90            ]),
91            account_submenu("claude:work"),
92        )
93        .separator()
94        .section_header(
95            Row::info()
96                .leading(Icon::from_png_bytes(codex_logo))
97                .segment(Segment::new("Codex").font(Font::system(13.0, Weight::Bold))),
98        )
99        .row(Row::new("switch:codex:me").segments(vec![
100            Segment::new("me@example.com").flex(Flex::Grow),
101            Segment::new("3h 12m")
102                .align(Align::Right)
103                .runs(vec![StyleRun::new(0, 6, Color::SystemOrange)]),
104        ]))
105        .separator()
106        .submenu(
107            Row::new("settings").label("Settings"),
108            Menu::new()
109                .row(Row::new("settings:apikey").label("API key…"))
110                .row(Row::new("settings:autoswap").label("Auto-swap accounts"))
111                .separator()
112                .row(Row::new("settings:notifications").label("Notifications…")),
113        )
114        .row(Row::new("quit").segments(vec![
115            Segment::new("Quit").flex(Flex::Grow),
116            Segment::new(concat!("usagio v", env!("CARGO_PKG_VERSION")))
117                .align(Align::Right)
118                .color(Color::SecondaryLabel),
119        ]))
120}
More examples
Hide additional examples
examples/usagio_menu.rs (line 59)
56fn account_submenu(acct: &Account) -> Menu {
57    let mut menu = Menu::new()
58        // Info rows read as normal text but are non-interactive (id = none).
59        .row(Row::info().label("Session resets in 2h 41m"))
60        .row(Row::info().label("Weekly resets in 3d 4h"))
61        .row(Row::info().segment(Segment::new("updated 12s ago").color(Color::SecondaryLabel)))
62        .separator();
63
64    if acct.active {
65        menu = menu.row(
66            Row::new("noop")
67                .leading(Icon::Checkmark)
68                .segment(Segment::new("Active").color(Color::SystemGreen))
69                .enabled(false),
70        );
71    } else {
72        menu = menu.row(
73            Row::new(format!("switch:{}:{}", acct.provider, acct.key))
74                .label("Switch to this account"),
75        );
76    }
77    if acct.supports_launch {
78        menu = menu.row(
79            Row::new(format!("launch:{}:{}", acct.provider, acct.key))
80                .label("Launch client")
81                // A named symbol trails the action (SF Symbol on macOS).
82                .trailing(Icon::Symbol("arrow.up.forward.app")),
83        );
84    }
85    if acct.supports_remove {
86        menu = menu.row(
87            Row::new(format!("remove:{}:{}", acct.provider, acct.key)).label("Remove\u{2026}"),
88        );
89    }
90    menu
91}
92
93/// The Capture submenu: one entry per provider to capture the current login.
94fn capture_submenu(groups: &[Group]) -> Menu {
95    let mut menu = Menu::new();
96    for group in groups {
97        menu = menu.row(
98            Row::new(format!("capture:{}", group.display_name.to_lowercase()))
99                .leading(Icon::from_png_bytes(group.icon_png))
100                .label(format!("Capture {} login", group.display_name)),
101        );
102    }
103    menu
104}
105
106/// The Settings submenu: a checkable toggle, an auto-swap threshold flyout, and
107/// a backup flyout — exercising nested submenus, `checked`, and an SVG icon.
108fn settings_submenu() -> Menu {
109    let autoswap = Menu::new()
110        .row(Row::new("autoswap:off").checked(false).label("Off"))
111        .row(Row::new("autoswap:80").checked(false).label("At 80%"))
112        .row(Row::new("autoswap:90").checked(true).label("At 90%"))
113        .separator()
114        .row(Row::new("autoswap:now").label("Swap now"));
115
116    let backup = Menu::new()
117        .row(Row::new("backup:save").label("Save backup\u{2026}"))
118        .row(Row::new("backup:restore").label("Restore backup\u{2026}"));
119
120    Menu::new()
121        .row(
122            Row::new("notifications:limits")
123                .checked(true)
124                .label("Notify near limits"),
125        )
126        .submenu(Row::new("autoswap").label("Auto-swap threshold"), autoswap)
127        .submenu(
128            Row::new("backup")
129                // An SVG leading icon, rasterized per-DPI at draw time.
130                .leading(Icon::from_svg_bytes(
131                    br#"<svg xmlns="http://www.w3.org/2000/svg"/>"#.as_slice(),
132                ))
133                .label("Backup"),
134            backup,
135        )
136        .separator()
137        .row(Row::new("refresh:now").label("Refresh now"))
138}
139
140fn build(groups: &[Group]) -> Menu {
141    let mut menu = Menu::new();
142
143    for group in groups {
144        // Bold, logo'd, non-interactive group header (Claude, then Codex).
145        menu = menu.section_header(
146            Row::info()
147                .leading(Icon::from_png_bytes(group.icon_png))
148                .segment(Segment::new(group.display_name).font(Font::system(13.0, Weight::Bold))),
149        );
150
151        for acct in &group.accounts {
152            // The label Grows to eat the leftover width, so the value segment
153            // sits flush at the true right edge — no reserved chevron column.
154            let label = Segment::new(acct.display)
155                .flex(Flex::Grow)
156                .font(if acct.active {
157                    Font::system(13.0, Weight::Bold)
158                } else {
159                    Font::system(13.0, Weight::Regular)
160                });
161
162            let value = Segment::new(acct.trailing)
163                .align(Align::Right)
164                .runs(severity_runs(&acct.severity));
165
166            let mut label_row = Row::new(format!("switch:{}:{}", acct.provider, acct.key))
167                .segments(vec![label, value])
168                .checked(acct.active);
169            if acct.active {
170                label_row = label_row.leading(Icon::Checkmark);
171            }
172
173            menu = menu.submenu(label_row, account_submenu(acct));
174        }
175
176        menu = menu.separator();
177    }
178
179    // Bottom actions — now with populated submenus.
180    menu = menu
181        .submenu(
182            Row::new("capture").label("Capture current login"),
183            capture_submenu(groups),
184        )
185        .submenu(Row::new("settings").label("Settings"), settings_submenu());
186
187    // "Quit ........ usagio vX" — label Grows, version tail is greyed + flush-right.
188    menu = menu.row(Row::new("quit").segments(vec![
189        Segment::new("Quit").flex(Flex::Grow),
190        Segment::new(format!("usagio v{}", env!("CARGO_PKG_VERSION")))
191            .align(Align::Right)
192            .color(Color::SecondaryLabel),
193    ]));
194
195    menu
196}
197
198fn icon_tag(icon: &Option<Icon>) -> &'static str {
199    match icon {
200        None => "",
201        Some(Icon::Checkmark) => " [check]",
202        Some(Icon::Png(_)) => " [png]",
203        Some(Icon::Svg(_)) => " [svg]",
204        Some(Icon::Symbol(_)) => " [symbol]",
205    }
206}
207
208fn print_row(row: &Row, prefix: &str, pad: &str) {
209    let check = match row.checked {
210        Some(true) => "[x] ",
211        Some(false) => "[ ] ",
212        None => "",
213    };
214    let dim = if row.enabled { "" } else { " (disabled)" };
215    println!(
216        "{pad}{prefix}{}{check}{}{}{}  [{}]",
217        icon_tag(&row.leading),
218        row_text(row),
219        icon_tag(&row.trailing),
220        dim,
221        row.id.as_str(),
222    );
223}
224
225fn print_menu(menu: &Menu, depth: usize) {
226    let pad = "  ".repeat(depth);
227    for item in &menu.items {
228        match item {
229            Item::Separator => println!("{pad}----"),
230            Item::SectionHeader(row) => {
231                println!("{pad}#{} {}", icon_tag(&row.leading), row_text(row))
232            }
233            Item::Row(row) => print_row(row, "- ", &pad),
234            Item::Submenu { label, menu } => {
235                print_row(label, "> ", &pad);
236                print_menu(menu, depth + 1);
237            }
238        }
239    }
240}
241
242fn row_text(row: &Row) -> String {
243    row.segments
244        .iter()
245        .map(|s| s.text.as_str())
246        .collect::<Vec<_>>()
247        .join("  ")
248}
249
250fn demo_theme_resolution() {
251    // A consumer can resolve semantic colors against any theme with no GUI.
252    let dark = Theme::dark();
253    let label = dark.resolve(Color::Label);
254    let red = dark.resolve(Color::SystemRed);
255    println!(
256        "theme resolution (dark): Label -> rgba({},{},{},{}), SystemRed -> rgba({},{},{},{})",
257        label.r, label.g, label.b, label.a, red.r, red.g, red.b, red.a,
258    );
259}
260
261fn demo_flush_right_layout() {
262    // The "Quit ...... usagio v1" row: a Grow label + a Fixed, right-aligned
263    // version tail. Given measured intrinsic widths, the layout engine flushes
264    // the tail to the right edge with no reserved column.
265    let content_width = 220.0;
266    let segs = [
267        SegmentMetrics::new(30.0, Flex::Grow, Align::Left), // "Quit"
268        SegmentMetrics::new(60.0, Flex::Fixed, Align::Right), // "usagio v1"
269    ];
270    let boxes = resolve_segments(&segs, content_width);
271    println!(
272        "flush-right layout: content {content_width}px -> tail text starts at x={} (right edge {})",
273        boxes[1].text_x,
274        content_width - 60.0,
275    );
276}
277
278fn demo_context_menu() {
279    // The pointer-anchored primitive that also works on Linux.
280    let menu = Menu::new()
281        .row(Row::new("copy").label("Copy"))
282        .row(Row::new("paste").label("Paste"))
283        .separator()
284        .row(Row::new("select-all").label("Select All"));
285    let cm = ContextMenu::new(menu).on_click(|id| println!("context click: {}", id.as_str()));
286    // Exercise the dispatch path without a GUI.
287    cm.dispatch(&"copy".into());
288    println!("context menu has {} items", cm.menu().len());
289}
Source

pub fn segment(self, segment: Segment) -> Self

Append a pre-built segment.

Examples found in repository?
examples/demo_tray.rs (line 53)
39fn account_submenu(slug: &str) -> Menu {
40    Menu::new()
41        .row(Row::info().segments(vec![
42            Segment::new("Session resets in").flex(Flex::Grow),
43            Segment::new("3h 12m")
44                .align(Align::Right)
45                .color(Color::SecondaryLabel),
46        ]))
47        .row(Row::info().segments(vec![
48            Segment::new("Weekly resets in").flex(Flex::Grow),
49            Segment::new("2d 4h")
50                .align(Align::Right)
51                .color(Color::SecondaryLabel),
52        ]))
53        .row(Row::info().segment(Segment::new("updated 1m ago").color(Color::SecondaryLabel)))
54        .separator()
55        .row(Row::new(format!("switch:{slug}")).label("Switch to this account"))
56        .row(Row::new(format!("launch:{slug}")).label("Launch client"))
57        .row(Row::new(format!("remove:{slug}")).label("Remove…"))
58}
59
60fn demo_menu() -> Menu {
61    let claude_logo = swatch_png(217, 119, 87);
62    let codex_logo = swatch_png(80, 80, 90);
63
64    Menu::new()
65        .section_header(
66            Row::info()
67                .leading(Icon::from_png_bytes(claude_logo))
68                .segment(Segment::new("Claude").font(Font::system(13.0, Weight::Bold))),
69        )
70        .submenu(
71            Row::new("acct:claude:me")
72                .leading(Icon::Checkmark)
73                .checked(true)
74                .segments(vec![
75                    Segment::new("me@example.com")
76                        .flex(Flex::Grow)
77                        .font(Font::system(13.0, Weight::Bold)),
78                    Segment::new("47% / 89%")
79                        .align(Align::Right)
80                        .runs(vec![StyleRun::new(6, 3, Color::SystemRed)]),
81                ]),
82            account_submenu("claude:me"),
83        )
84        .submenu(
85            Row::new("acct:claude:work").segments(vec![
86                Segment::new("work@example.com").flex(Flex::Grow),
87                Segment::new("12% / 30%")
88                    .align(Align::Right)
89                    .runs(vec![StyleRun::new(0, 3, Color::SystemGreen)]),
90            ]),
91            account_submenu("claude:work"),
92        )
93        .separator()
94        .section_header(
95            Row::info()
96                .leading(Icon::from_png_bytes(codex_logo))
97                .segment(Segment::new("Codex").font(Font::system(13.0, Weight::Bold))),
98        )
99        .row(Row::new("switch:codex:me").segments(vec![
100            Segment::new("me@example.com").flex(Flex::Grow),
101            Segment::new("3h 12m")
102                .align(Align::Right)
103                .runs(vec![StyleRun::new(0, 6, Color::SystemOrange)]),
104        ]))
105        .separator()
106        .submenu(
107            Row::new("settings").label("Settings"),
108            Menu::new()
109                .row(Row::new("settings:apikey").label("API key…"))
110                .row(Row::new("settings:autoswap").label("Auto-swap accounts"))
111                .separator()
112                .row(Row::new("settings:notifications").label("Notifications…")),
113        )
114        .row(Row::new("quit").segments(vec![
115            Segment::new("Quit").flex(Flex::Grow),
116            Segment::new(concat!("usagio v", env!("CARGO_PKG_VERSION")))
117                .align(Align::Right)
118                .color(Color::SecondaryLabel),
119        ]))
120}
More examples
Hide additional examples
examples/usagio_menu.rs (line 61)
56fn account_submenu(acct: &Account) -> Menu {
57    let mut menu = Menu::new()
58        // Info rows read as normal text but are non-interactive (id = none).
59        .row(Row::info().label("Session resets in 2h 41m"))
60        .row(Row::info().label("Weekly resets in 3d 4h"))
61        .row(Row::info().segment(Segment::new("updated 12s ago").color(Color::SecondaryLabel)))
62        .separator();
63
64    if acct.active {
65        menu = menu.row(
66            Row::new("noop")
67                .leading(Icon::Checkmark)
68                .segment(Segment::new("Active").color(Color::SystemGreen))
69                .enabled(false),
70        );
71    } else {
72        menu = menu.row(
73            Row::new(format!("switch:{}:{}", acct.provider, acct.key))
74                .label("Switch to this account"),
75        );
76    }
77    if acct.supports_launch {
78        menu = menu.row(
79            Row::new(format!("launch:{}:{}", acct.provider, acct.key))
80                .label("Launch client")
81                // A named symbol trails the action (SF Symbol on macOS).
82                .trailing(Icon::Symbol("arrow.up.forward.app")),
83        );
84    }
85    if acct.supports_remove {
86        menu = menu.row(
87            Row::new(format!("remove:{}:{}", acct.provider, acct.key)).label("Remove\u{2026}"),
88        );
89    }
90    menu
91}
92
93/// The Capture submenu: one entry per provider to capture the current login.
94fn capture_submenu(groups: &[Group]) -> Menu {
95    let mut menu = Menu::new();
96    for group in groups {
97        menu = menu.row(
98            Row::new(format!("capture:{}", group.display_name.to_lowercase()))
99                .leading(Icon::from_png_bytes(group.icon_png))
100                .label(format!("Capture {} login", group.display_name)),
101        );
102    }
103    menu
104}
105
106/// The Settings submenu: a checkable toggle, an auto-swap threshold flyout, and
107/// a backup flyout — exercising nested submenus, `checked`, and an SVG icon.
108fn settings_submenu() -> Menu {
109    let autoswap = Menu::new()
110        .row(Row::new("autoswap:off").checked(false).label("Off"))
111        .row(Row::new("autoswap:80").checked(false).label("At 80%"))
112        .row(Row::new("autoswap:90").checked(true).label("At 90%"))
113        .separator()
114        .row(Row::new("autoswap:now").label("Swap now"));
115
116    let backup = Menu::new()
117        .row(Row::new("backup:save").label("Save backup\u{2026}"))
118        .row(Row::new("backup:restore").label("Restore backup\u{2026}"));
119
120    Menu::new()
121        .row(
122            Row::new("notifications:limits")
123                .checked(true)
124                .label("Notify near limits"),
125        )
126        .submenu(Row::new("autoswap").label("Auto-swap threshold"), autoswap)
127        .submenu(
128            Row::new("backup")
129                // An SVG leading icon, rasterized per-DPI at draw time.
130                .leading(Icon::from_svg_bytes(
131                    br#"<svg xmlns="http://www.w3.org/2000/svg"/>"#.as_slice(),
132                ))
133                .label("Backup"),
134            backup,
135        )
136        .separator()
137        .row(Row::new("refresh:now").label("Refresh now"))
138}
139
140fn build(groups: &[Group]) -> Menu {
141    let mut menu = Menu::new();
142
143    for group in groups {
144        // Bold, logo'd, non-interactive group header (Claude, then Codex).
145        menu = menu.section_header(
146            Row::info()
147                .leading(Icon::from_png_bytes(group.icon_png))
148                .segment(Segment::new(group.display_name).font(Font::system(13.0, Weight::Bold))),
149        );
150
151        for acct in &group.accounts {
152            // The label Grows to eat the leftover width, so the value segment
153            // sits flush at the true right edge — no reserved chevron column.
154            let label = Segment::new(acct.display)
155                .flex(Flex::Grow)
156                .font(if acct.active {
157                    Font::system(13.0, Weight::Bold)
158                } else {
159                    Font::system(13.0, Weight::Regular)
160                });
161
162            let value = Segment::new(acct.trailing)
163                .align(Align::Right)
164                .runs(severity_runs(&acct.severity));
165
166            let mut label_row = Row::new(format!("switch:{}:{}", acct.provider, acct.key))
167                .segments(vec![label, value])
168                .checked(acct.active);
169            if acct.active {
170                label_row = label_row.leading(Icon::Checkmark);
171            }
172
173            menu = menu.submenu(label_row, account_submenu(acct));
174        }
175
176        menu = menu.separator();
177    }
178
179    // Bottom actions — now with populated submenus.
180    menu = menu
181        .submenu(
182            Row::new("capture").label("Capture current login"),
183            capture_submenu(groups),
184        )
185        .submenu(Row::new("settings").label("Settings"), settings_submenu());
186
187    // "Quit ........ usagio vX" — label Grows, version tail is greyed + flush-right.
188    menu = menu.row(Row::new("quit").segments(vec![
189        Segment::new("Quit").flex(Flex::Grow),
190        Segment::new(format!("usagio v{}", env!("CARGO_PKG_VERSION")))
191            .align(Align::Right)
192            .color(Color::SecondaryLabel),
193    ]));
194
195    menu
196}
Source

pub fn segments(self, segments: Vec<Segment>) -> Self

Replace all segments.

Examples found in repository?
examples/demo_tray.rs (lines 41-46)
39fn account_submenu(slug: &str) -> Menu {
40    Menu::new()
41        .row(Row::info().segments(vec![
42            Segment::new("Session resets in").flex(Flex::Grow),
43            Segment::new("3h 12m")
44                .align(Align::Right)
45                .color(Color::SecondaryLabel),
46        ]))
47        .row(Row::info().segments(vec![
48            Segment::new("Weekly resets in").flex(Flex::Grow),
49            Segment::new("2d 4h")
50                .align(Align::Right)
51                .color(Color::SecondaryLabel),
52        ]))
53        .row(Row::info().segment(Segment::new("updated 1m ago").color(Color::SecondaryLabel)))
54        .separator()
55        .row(Row::new(format!("switch:{slug}")).label("Switch to this account"))
56        .row(Row::new(format!("launch:{slug}")).label("Launch client"))
57        .row(Row::new(format!("remove:{slug}")).label("Remove…"))
58}
59
60fn demo_menu() -> Menu {
61    let claude_logo = swatch_png(217, 119, 87);
62    let codex_logo = swatch_png(80, 80, 90);
63
64    Menu::new()
65        .section_header(
66            Row::info()
67                .leading(Icon::from_png_bytes(claude_logo))
68                .segment(Segment::new("Claude").font(Font::system(13.0, Weight::Bold))),
69        )
70        .submenu(
71            Row::new("acct:claude:me")
72                .leading(Icon::Checkmark)
73                .checked(true)
74                .segments(vec![
75                    Segment::new("me@example.com")
76                        .flex(Flex::Grow)
77                        .font(Font::system(13.0, Weight::Bold)),
78                    Segment::new("47% / 89%")
79                        .align(Align::Right)
80                        .runs(vec![StyleRun::new(6, 3, Color::SystemRed)]),
81                ]),
82            account_submenu("claude:me"),
83        )
84        .submenu(
85            Row::new("acct:claude:work").segments(vec![
86                Segment::new("work@example.com").flex(Flex::Grow),
87                Segment::new("12% / 30%")
88                    .align(Align::Right)
89                    .runs(vec![StyleRun::new(0, 3, Color::SystemGreen)]),
90            ]),
91            account_submenu("claude:work"),
92        )
93        .separator()
94        .section_header(
95            Row::info()
96                .leading(Icon::from_png_bytes(codex_logo))
97                .segment(Segment::new("Codex").font(Font::system(13.0, Weight::Bold))),
98        )
99        .row(Row::new("switch:codex:me").segments(vec![
100            Segment::new("me@example.com").flex(Flex::Grow),
101            Segment::new("3h 12m")
102                .align(Align::Right)
103                .runs(vec![StyleRun::new(0, 6, Color::SystemOrange)]),
104        ]))
105        .separator()
106        .submenu(
107            Row::new("settings").label("Settings"),
108            Menu::new()
109                .row(Row::new("settings:apikey").label("API key…"))
110                .row(Row::new("settings:autoswap").label("Auto-swap accounts"))
111                .separator()
112                .row(Row::new("settings:notifications").label("Notifications…")),
113        )
114        .row(Row::new("quit").segments(vec![
115            Segment::new("Quit").flex(Flex::Grow),
116            Segment::new(concat!("usagio v", env!("CARGO_PKG_VERSION")))
117                .align(Align::Right)
118                .color(Color::SecondaryLabel),
119        ]))
120}
More examples
Hide additional examples
examples/usagio_menu.rs (line 167)
140fn build(groups: &[Group]) -> Menu {
141    let mut menu = Menu::new();
142
143    for group in groups {
144        // Bold, logo'd, non-interactive group header (Claude, then Codex).
145        menu = menu.section_header(
146            Row::info()
147                .leading(Icon::from_png_bytes(group.icon_png))
148                .segment(Segment::new(group.display_name).font(Font::system(13.0, Weight::Bold))),
149        );
150
151        for acct in &group.accounts {
152            // The label Grows to eat the leftover width, so the value segment
153            // sits flush at the true right edge — no reserved chevron column.
154            let label = Segment::new(acct.display)
155                .flex(Flex::Grow)
156                .font(if acct.active {
157                    Font::system(13.0, Weight::Bold)
158                } else {
159                    Font::system(13.0, Weight::Regular)
160                });
161
162            let value = Segment::new(acct.trailing)
163                .align(Align::Right)
164                .runs(severity_runs(&acct.severity));
165
166            let mut label_row = Row::new(format!("switch:{}:{}", acct.provider, acct.key))
167                .segments(vec![label, value])
168                .checked(acct.active);
169            if acct.active {
170                label_row = label_row.leading(Icon::Checkmark);
171            }
172
173            menu = menu.submenu(label_row, account_submenu(acct));
174        }
175
176        menu = menu.separator();
177    }
178
179    // Bottom actions — now with populated submenus.
180    menu = menu
181        .submenu(
182            Row::new("capture").label("Capture current login"),
183            capture_submenu(groups),
184        )
185        .submenu(Row::new("settings").label("Settings"), settings_submenu());
186
187    // "Quit ........ usagio vX" — label Grows, version tail is greyed + flush-right.
188    menu = menu.row(Row::new("quit").segments(vec![
189        Segment::new("Quit").flex(Flex::Grow),
190        Segment::new(format!("usagio v{}", env!("CARGO_PKG_VERSION")))
191            .align(Align::Right)
192            .color(Color::SecondaryLabel),
193    ]));
194
195    menu
196}
Source

pub fn leading(self, icon: Icon) -> Self

Set the leading icon.

Examples found in repository?
examples/usagio_menu.rs (line 67)
56fn account_submenu(acct: &Account) -> Menu {
57    let mut menu = Menu::new()
58        // Info rows read as normal text but are non-interactive (id = none).
59        .row(Row::info().label("Session resets in 2h 41m"))
60        .row(Row::info().label("Weekly resets in 3d 4h"))
61        .row(Row::info().segment(Segment::new("updated 12s ago").color(Color::SecondaryLabel)))
62        .separator();
63
64    if acct.active {
65        menu = menu.row(
66            Row::new("noop")
67                .leading(Icon::Checkmark)
68                .segment(Segment::new("Active").color(Color::SystemGreen))
69                .enabled(false),
70        );
71    } else {
72        menu = menu.row(
73            Row::new(format!("switch:{}:{}", acct.provider, acct.key))
74                .label("Switch to this account"),
75        );
76    }
77    if acct.supports_launch {
78        menu = menu.row(
79            Row::new(format!("launch:{}:{}", acct.provider, acct.key))
80                .label("Launch client")
81                // A named symbol trails the action (SF Symbol on macOS).
82                .trailing(Icon::Symbol("arrow.up.forward.app")),
83        );
84    }
85    if acct.supports_remove {
86        menu = menu.row(
87            Row::new(format!("remove:{}:{}", acct.provider, acct.key)).label("Remove\u{2026}"),
88        );
89    }
90    menu
91}
92
93/// The Capture submenu: one entry per provider to capture the current login.
94fn capture_submenu(groups: &[Group]) -> Menu {
95    let mut menu = Menu::new();
96    for group in groups {
97        menu = menu.row(
98            Row::new(format!("capture:{}", group.display_name.to_lowercase()))
99                .leading(Icon::from_png_bytes(group.icon_png))
100                .label(format!("Capture {} login", group.display_name)),
101        );
102    }
103    menu
104}
105
106/// The Settings submenu: a checkable toggle, an auto-swap threshold flyout, and
107/// a backup flyout — exercising nested submenus, `checked`, and an SVG icon.
108fn settings_submenu() -> Menu {
109    let autoswap = Menu::new()
110        .row(Row::new("autoswap:off").checked(false).label("Off"))
111        .row(Row::new("autoswap:80").checked(false).label("At 80%"))
112        .row(Row::new("autoswap:90").checked(true).label("At 90%"))
113        .separator()
114        .row(Row::new("autoswap:now").label("Swap now"));
115
116    let backup = Menu::new()
117        .row(Row::new("backup:save").label("Save backup\u{2026}"))
118        .row(Row::new("backup:restore").label("Restore backup\u{2026}"));
119
120    Menu::new()
121        .row(
122            Row::new("notifications:limits")
123                .checked(true)
124                .label("Notify near limits"),
125        )
126        .submenu(Row::new("autoswap").label("Auto-swap threshold"), autoswap)
127        .submenu(
128            Row::new("backup")
129                // An SVG leading icon, rasterized per-DPI at draw time.
130                .leading(Icon::from_svg_bytes(
131                    br#"<svg xmlns="http://www.w3.org/2000/svg"/>"#.as_slice(),
132                ))
133                .label("Backup"),
134            backup,
135        )
136        .separator()
137        .row(Row::new("refresh:now").label("Refresh now"))
138}
139
140fn build(groups: &[Group]) -> Menu {
141    let mut menu = Menu::new();
142
143    for group in groups {
144        // Bold, logo'd, non-interactive group header (Claude, then Codex).
145        menu = menu.section_header(
146            Row::info()
147                .leading(Icon::from_png_bytes(group.icon_png))
148                .segment(Segment::new(group.display_name).font(Font::system(13.0, Weight::Bold))),
149        );
150
151        for acct in &group.accounts {
152            // The label Grows to eat the leftover width, so the value segment
153            // sits flush at the true right edge — no reserved chevron column.
154            let label = Segment::new(acct.display)
155                .flex(Flex::Grow)
156                .font(if acct.active {
157                    Font::system(13.0, Weight::Bold)
158                } else {
159                    Font::system(13.0, Weight::Regular)
160                });
161
162            let value = Segment::new(acct.trailing)
163                .align(Align::Right)
164                .runs(severity_runs(&acct.severity));
165
166            let mut label_row = Row::new(format!("switch:{}:{}", acct.provider, acct.key))
167                .segments(vec![label, value])
168                .checked(acct.active);
169            if acct.active {
170                label_row = label_row.leading(Icon::Checkmark);
171            }
172
173            menu = menu.submenu(label_row, account_submenu(acct));
174        }
175
176        menu = menu.separator();
177    }
178
179    // Bottom actions — now with populated submenus.
180    menu = menu
181        .submenu(
182            Row::new("capture").label("Capture current login"),
183            capture_submenu(groups),
184        )
185        .submenu(Row::new("settings").label("Settings"), settings_submenu());
186
187    // "Quit ........ usagio vX" — label Grows, version tail is greyed + flush-right.
188    menu = menu.row(Row::new("quit").segments(vec![
189        Segment::new("Quit").flex(Flex::Grow),
190        Segment::new(format!("usagio v{}", env!("CARGO_PKG_VERSION")))
191            .align(Align::Right)
192            .color(Color::SecondaryLabel),
193    ]));
194
195    menu
196}
More examples
Hide additional examples
examples/demo_tray.rs (line 67)
60fn demo_menu() -> Menu {
61    let claude_logo = swatch_png(217, 119, 87);
62    let codex_logo = swatch_png(80, 80, 90);
63
64    Menu::new()
65        .section_header(
66            Row::info()
67                .leading(Icon::from_png_bytes(claude_logo))
68                .segment(Segment::new("Claude").font(Font::system(13.0, Weight::Bold))),
69        )
70        .submenu(
71            Row::new("acct:claude:me")
72                .leading(Icon::Checkmark)
73                .checked(true)
74                .segments(vec![
75                    Segment::new("me@example.com")
76                        .flex(Flex::Grow)
77                        .font(Font::system(13.0, Weight::Bold)),
78                    Segment::new("47% / 89%")
79                        .align(Align::Right)
80                        .runs(vec![StyleRun::new(6, 3, Color::SystemRed)]),
81                ]),
82            account_submenu("claude:me"),
83        )
84        .submenu(
85            Row::new("acct:claude:work").segments(vec![
86                Segment::new("work@example.com").flex(Flex::Grow),
87                Segment::new("12% / 30%")
88                    .align(Align::Right)
89                    .runs(vec![StyleRun::new(0, 3, Color::SystemGreen)]),
90            ]),
91            account_submenu("claude:work"),
92        )
93        .separator()
94        .section_header(
95            Row::info()
96                .leading(Icon::from_png_bytes(codex_logo))
97                .segment(Segment::new("Codex").font(Font::system(13.0, Weight::Bold))),
98        )
99        .row(Row::new("switch:codex:me").segments(vec![
100            Segment::new("me@example.com").flex(Flex::Grow),
101            Segment::new("3h 12m")
102                .align(Align::Right)
103                .runs(vec![StyleRun::new(0, 6, Color::SystemOrange)]),
104        ]))
105        .separator()
106        .submenu(
107            Row::new("settings").label("Settings"),
108            Menu::new()
109                .row(Row::new("settings:apikey").label("API key…"))
110                .row(Row::new("settings:autoswap").label("Auto-swap accounts"))
111                .separator()
112                .row(Row::new("settings:notifications").label("Notifications…")),
113        )
114        .row(Row::new("quit").segments(vec![
115            Segment::new("Quit").flex(Flex::Grow),
116            Segment::new(concat!("usagio v", env!("CARGO_PKG_VERSION")))
117                .align(Align::Right)
118                .color(Color::SecondaryLabel),
119        ]))
120}
Source

pub fn trailing(self, icon: Icon) -> Self

Set the trailing icon.

Examples found in repository?
examples/usagio_menu.rs (line 82)
56fn account_submenu(acct: &Account) -> Menu {
57    let mut menu = Menu::new()
58        // Info rows read as normal text but are non-interactive (id = none).
59        .row(Row::info().label("Session resets in 2h 41m"))
60        .row(Row::info().label("Weekly resets in 3d 4h"))
61        .row(Row::info().segment(Segment::new("updated 12s ago").color(Color::SecondaryLabel)))
62        .separator();
63
64    if acct.active {
65        menu = menu.row(
66            Row::new("noop")
67                .leading(Icon::Checkmark)
68                .segment(Segment::new("Active").color(Color::SystemGreen))
69                .enabled(false),
70        );
71    } else {
72        menu = menu.row(
73            Row::new(format!("switch:{}:{}", acct.provider, acct.key))
74                .label("Switch to this account"),
75        );
76    }
77    if acct.supports_launch {
78        menu = menu.row(
79            Row::new(format!("launch:{}:{}", acct.provider, acct.key))
80                .label("Launch client")
81                // A named symbol trails the action (SF Symbol on macOS).
82                .trailing(Icon::Symbol("arrow.up.forward.app")),
83        );
84    }
85    if acct.supports_remove {
86        menu = menu.row(
87            Row::new(format!("remove:{}:{}", acct.provider, acct.key)).label("Remove\u{2026}"),
88        );
89    }
90    menu
91}
Source

pub fn enabled(self, enabled: bool) -> Self

Set enabled state.

Examples found in repository?
examples/usagio_menu.rs (line 69)
56fn account_submenu(acct: &Account) -> Menu {
57    let mut menu = Menu::new()
58        // Info rows read as normal text but are non-interactive (id = none).
59        .row(Row::info().label("Session resets in 2h 41m"))
60        .row(Row::info().label("Weekly resets in 3d 4h"))
61        .row(Row::info().segment(Segment::new("updated 12s ago").color(Color::SecondaryLabel)))
62        .separator();
63
64    if acct.active {
65        menu = menu.row(
66            Row::new("noop")
67                .leading(Icon::Checkmark)
68                .segment(Segment::new("Active").color(Color::SystemGreen))
69                .enabled(false),
70        );
71    } else {
72        menu = menu.row(
73            Row::new(format!("switch:{}:{}", acct.provider, acct.key))
74                .label("Switch to this account"),
75        );
76    }
77    if acct.supports_launch {
78        menu = menu.row(
79            Row::new(format!("launch:{}:{}", acct.provider, acct.key))
80                .label("Launch client")
81                // A named symbol trails the action (SF Symbol on macOS).
82                .trailing(Icon::Symbol("arrow.up.forward.app")),
83        );
84    }
85    if acct.supports_remove {
86        menu = menu.row(
87            Row::new(format!("remove:{}:{}", acct.provider, acct.key)).label("Remove\u{2026}"),
88        );
89    }
90    menu
91}
Source

pub fn checked(self, checked: bool) -> Self

Show a check column in the given state.

Examples found in repository?
examples/usagio_menu.rs (line 110)
108fn settings_submenu() -> Menu {
109    let autoswap = Menu::new()
110        .row(Row::new("autoswap:off").checked(false).label("Off"))
111        .row(Row::new("autoswap:80").checked(false).label("At 80%"))
112        .row(Row::new("autoswap:90").checked(true).label("At 90%"))
113        .separator()
114        .row(Row::new("autoswap:now").label("Swap now"));
115
116    let backup = Menu::new()
117        .row(Row::new("backup:save").label("Save backup\u{2026}"))
118        .row(Row::new("backup:restore").label("Restore backup\u{2026}"));
119
120    Menu::new()
121        .row(
122            Row::new("notifications:limits")
123                .checked(true)
124                .label("Notify near limits"),
125        )
126        .submenu(Row::new("autoswap").label("Auto-swap threshold"), autoswap)
127        .submenu(
128            Row::new("backup")
129                // An SVG leading icon, rasterized per-DPI at draw time.
130                .leading(Icon::from_svg_bytes(
131                    br#"<svg xmlns="http://www.w3.org/2000/svg"/>"#.as_slice(),
132                ))
133                .label("Backup"),
134            backup,
135        )
136        .separator()
137        .row(Row::new("refresh:now").label("Refresh now"))
138}
139
140fn build(groups: &[Group]) -> Menu {
141    let mut menu = Menu::new();
142
143    for group in groups {
144        // Bold, logo'd, non-interactive group header (Claude, then Codex).
145        menu = menu.section_header(
146            Row::info()
147                .leading(Icon::from_png_bytes(group.icon_png))
148                .segment(Segment::new(group.display_name).font(Font::system(13.0, Weight::Bold))),
149        );
150
151        for acct in &group.accounts {
152            // The label Grows to eat the leftover width, so the value segment
153            // sits flush at the true right edge — no reserved chevron column.
154            let label = Segment::new(acct.display)
155                .flex(Flex::Grow)
156                .font(if acct.active {
157                    Font::system(13.0, Weight::Bold)
158                } else {
159                    Font::system(13.0, Weight::Regular)
160                });
161
162            let value = Segment::new(acct.trailing)
163                .align(Align::Right)
164                .runs(severity_runs(&acct.severity));
165
166            let mut label_row = Row::new(format!("switch:{}:{}", acct.provider, acct.key))
167                .segments(vec![label, value])
168                .checked(acct.active);
169            if acct.active {
170                label_row = label_row.leading(Icon::Checkmark);
171            }
172
173            menu = menu.submenu(label_row, account_submenu(acct));
174        }
175
176        menu = menu.separator();
177    }
178
179    // Bottom actions — now with populated submenus.
180    menu = menu
181        .submenu(
182            Row::new("capture").label("Capture current login"),
183            capture_submenu(groups),
184        )
185        .submenu(Row::new("settings").label("Settings"), settings_submenu());
186
187    // "Quit ........ usagio vX" — label Grows, version tail is greyed + flush-right.
188    menu = menu.row(Row::new("quit").segments(vec![
189        Segment::new("Quit").flex(Flex::Grow),
190        Segment::new(format!("usagio v{}", env!("CARGO_PKG_VERSION")))
191            .align(Align::Right)
192            .color(Color::SecondaryLabel),
193    ]));
194
195    menu
196}
More examples
Hide additional examples
examples/demo_tray.rs (line 73)
60fn demo_menu() -> Menu {
61    let claude_logo = swatch_png(217, 119, 87);
62    let codex_logo = swatch_png(80, 80, 90);
63
64    Menu::new()
65        .section_header(
66            Row::info()
67                .leading(Icon::from_png_bytes(claude_logo))
68                .segment(Segment::new("Claude").font(Font::system(13.0, Weight::Bold))),
69        )
70        .submenu(
71            Row::new("acct:claude:me")
72                .leading(Icon::Checkmark)
73                .checked(true)
74                .segments(vec![
75                    Segment::new("me@example.com")
76                        .flex(Flex::Grow)
77                        .font(Font::system(13.0, Weight::Bold)),
78                    Segment::new("47% / 89%")
79                        .align(Align::Right)
80                        .runs(vec![StyleRun::new(6, 3, Color::SystemRed)]),
81                ]),
82            account_submenu("claude:me"),
83        )
84        .submenu(
85            Row::new("acct:claude:work").segments(vec![
86                Segment::new("work@example.com").flex(Flex::Grow),
87                Segment::new("12% / 30%")
88                    .align(Align::Right)
89                    .runs(vec![StyleRun::new(0, 3, Color::SystemGreen)]),
90            ]),
91            account_submenu("claude:work"),
92        )
93        .separator()
94        .section_header(
95            Row::info()
96                .leading(Icon::from_png_bytes(codex_logo))
97                .segment(Segment::new("Codex").font(Font::system(13.0, Weight::Bold))),
98        )
99        .row(Row::new("switch:codex:me").segments(vec![
100            Segment::new("me@example.com").flex(Flex::Grow),
101            Segment::new("3h 12m")
102                .align(Align::Right)
103                .runs(vec![StyleRun::new(0, 6, Color::SystemOrange)]),
104        ]))
105        .separator()
106        .submenu(
107            Row::new("settings").label("Settings"),
108            Menu::new()
109                .row(Row::new("settings:apikey").label("API key…"))
110                .row(Row::new("settings:autoswap").label("Auto-swap accounts"))
111                .separator()
112                .row(Row::new("settings:notifications").label("Notifications…")),
113        )
114        .row(Row::new("quit").segments(vec![
115            Segment::new("Quit").flex(Flex::Grow),
116            Segment::new(concat!("usagio v", env!("CARGO_PKG_VERSION")))
117                .align(Align::Right)
118                .color(Color::SecondaryLabel),
119        ]))
120}
Source

pub fn background(self, color: Color) -> Self

Set an explicit background color.

Source

pub fn min_height(self, height: f32) -> Self

Set an explicit minimum row height.

Source

pub fn accessibility_label(self, label: impl Into<String>) -> Self

Override the accessible name announced by screen readers, in place of Row::accessible_name. Required for icon-only rows (no segments), whose derived name would otherwise be empty (spec 30 §1.4).

Source

pub fn accessible_name(&self) -> String

The row’s accessible name: its segment texts concatenated with spaces. This is what screen readers announce for the row (see the design’s a11y section).

Trait Implementations§

Source§

impl Clone for Row

Source§

fn clone(&self) -> Row

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Row

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Row

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

§

impl Freeze for Row

§

impl RefUnwindSafe for Row

§

impl Send for Row

§

impl Sync for Row

§

impl Unpin for Row

§

impl UnsafeUnpin for Row

§

impl UnwindSafe for Row

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> AutoreleaseSafe for T
where T: ?Sized,

Source§

impl<T> AutoreleaseSafe for T
where T: ?Sized,

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.