1use muri::render::Framebuffer;
18use muri::{Align, Color, Flex, Font, Icon, Menu, Row, Segment, StyleRun, Tray, Weight};
19
20fn swatch_png(r: u8, g: u8, b: u8) -> Vec<u8> {
22 let mut fb = Framebuffer::new(16, 16);
23 let px = fb.pixels_mut();
24 for i in 0..256usize {
25 let (x, y) = ((i % 16) as i32 - 8, (i / 16) as i32 - 8);
26 let a: u16 = if x * x + y * y <= 49 { 255 } else { 0 };
27 let o = i * 4;
28 px[o] = (r as u16 * a / 255) as u8;
30 px[o + 1] = (g as u16 * a / 255) as u8;
31 px[o + 2] = (b as u16 * a / 255) as u8;
32 px[o + 3] = a as u8;
33 }
34 fb.encode_png()
35}
36
37fn account_submenu(slug: &str) -> Menu {
40 Menu::new()
41 .row(Row::default().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::default().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::default().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::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 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}