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: MenuIdThe 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: boolWhether 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
impl Row
Sourcepub fn new(id: impl Into<MenuId>) -> Self
pub fn new(id: impl Into<MenuId>) -> Self
A new enabled row with the given click id and no segments.
Examples found in repository?
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
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}Sourcepub fn info() -> Self
pub fn info() -> Self
A non-interactive row (id = MenuId::none); handy for section headers
and pure info lines.
Examples found in repository?
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
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}Sourcepub fn label(self, text: impl Into<String>) -> Self
pub fn label(self, text: impl Into<String>) -> Self
Append a plain-text left-aligned segment (convenience).
Examples found in repository?
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
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}Sourcepub fn segment(self, segment: Segment) -> Self
pub fn segment(self, segment: Segment) -> Self
Append a pre-built segment.
Examples found in repository?
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
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}Sourcepub fn segments(self, segments: Vec<Segment>) -> Self
pub fn segments(self, segments: Vec<Segment>) -> Self
Replace all segments.
Examples found in repository?
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
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}Sourcepub fn leading(self, icon: Icon) -> Self
pub fn leading(self, icon: Icon) -> Self
Set the leading icon.
Examples found in repository?
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
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}Sourcepub fn trailing(self, icon: Icon) -> Self
pub fn trailing(self, icon: Icon) -> Self
Set the trailing icon.
Examples found in repository?
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}Sourcepub fn enabled(self, enabled: bool) -> Self
pub fn enabled(self, enabled: bool) -> Self
Set enabled state.
Examples found in repository?
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}Sourcepub fn checked(self, checked: bool) -> Self
pub fn checked(self, checked: bool) -> Self
Show a check column in the given state.
Examples found in repository?
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
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}Sourcepub fn background(self, color: Color) -> Self
pub fn background(self, color: Color) -> Self
Set an explicit background color.
Sourcepub fn min_height(self, height: f32) -> Self
pub fn min_height(self, height: f32) -> Self
Set an explicit minimum row height.
Sourcepub fn accessibility_label(self, label: impl Into<String>) -> Self
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).
Sourcepub fn accessible_name(&self) -> String
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).