pub enum Icon {
Png(Arc<[u8]>),
Svg(Arc<[u8]>),
Checkmark,
Symbol(&'static str),
}Expand description
A leading/trailing icon or logo. Raster (PNG) bytes are decoded at load; SVG
bytes are rasterized by muri’s own zeno-backed restricted-subset
rasterizer (paths, basic shapes, solid fills, transform; no filters/text/
gradients — those should ship as PNG). Both feed the same icon path.
Variants§
Png(Arc<[u8]>)
A PNG (or other auto-detected raster format) from raw bytes.
Svg(Arc<[u8]>)
An SVG from raw bytes, rasterized per target size via muri’s restricted
SVG subset (see the Icon note for what’s supported).
Checkmark
The themed checkmark glyph, drawn in the leading column.
Symbol(&'static str)
A named symbol: an SF Symbol on macOS, with a bundled fallback elsewhere.
Implementations§
Source§impl Icon
impl Icon
Sourcepub fn from_png(bytes: impl Into<Arc<[u8]>>) -> Self
pub fn from_png(bytes: impl Into<Arc<[u8]>>) -> Self
The one obvious way to build a raster icon: from encoded PNG (or any auto-detected raster format) bytes. The 90% path for a logo/avatar.
Examples found in repository?
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(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(
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::default()
147 .leading(Icon::from_png(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::default()
67 .leading(Icon::from_png(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::default()
96 .leading(Icon::from_png(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}
121
122fn main() {
123 let icon = Icon::from_png(swatch_png(120, 170, 255));
124 let tray = Tray::new(icon)
125 .tooltip("muri demo")
126 .menu(demo_menu())
127 .on_click(|id| {
128 println!("clicked: {}", id.as_str());
129 if id.as_str() == "quit" {
130 std::process::exit(0);
131 }
132 });
133
134 println!("muri demo_tray: click the status-bar icon to open the styled popup.");
135 // The tray owns the platform status item, which has main-thread affinity on
136 // macOS; `Tray::run` proves that at compile time by taking a MainThreadMarker
137 // (issue #46). `main` runs on the main thread, so this is always `Some`.
138 let Some(mtm) = muri::MainThreadMarker::new() else {
139 eprintln!("tray error: must be started on the main thread");
140 return;
141 };
142 if let Err(e) = tray.run(mtm) {
143 eprintln!("tray error: {e}");
144 }
145}Sourcepub fn from_rgba(rgba: &[u8], width: u32, height: u32) -> Result<Self>
pub fn from_rgba(rgba: &[u8], width: u32, height: u32) -> Result<Self>
The one obvious way to build an icon from raw straight-alpha RGBA8
pixels: width * height * 4 bytes, row-major. muri keeps a single
encoded-bytes representation internally (the pixels are encoded to PNG),
so a consumer never has to choose a representation — hence this returns a
plain Icon, indistinguishable at the type level from one built with
Icon::from_png. Returns Error::BadIcon when
the buffer length doesn’t equal width * height * 4 (or either dimension
is zero).
Sourcepub fn from_svg(bytes: impl Into<Arc<[u8]>>) -> Self
pub fn from_svg(bytes: impl Into<Arc<[u8]>>) -> Self
The one obvious way to build an icon from raw SVG bytes (rasterized
per target size via muri’s restricted SVG subset; see the Icon note).
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(
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}