From 93105e1c3a29dcd53af4c27a8df98d6d370205b8 Mon Sep 17 00:00:00 2001 From: FAZuH Date: Fri, 11 Sep 2026 23:15:29 +0700 Subject: [PATCH 1/2] feat(daemon): Discord-style mute/deafen glyphs in roster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the M/D text badges with MDI microphone-off / headphones-off vector glyphs (Apache-2.0 path data, tinted through iced's svg loader), drawn inline after the username at 16px. One mark per row — deafening overrides muting — red (red-600) when server-set, grey (neutral-400) when self-set. The mark decision lives in overlay/glyph.rs so the presence/override/color rules stay unit-pinned. Adds iced's svg feature (resvg tree, +1.9MB on the release daemon). Records the glyph change in the changelog Unreleased Overlay section. --- CHANGELOG.md | 4 + CONTEXT.md | 9 +- Cargo.lock | 241 +++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + docs/dev/code-layout.md | 1 + src/daemon/overlay/glyph.rs | 190 ++++++++++++++++++++++++++++ src/daemon/overlay/mod.rs | 1 + src/daemon/overlay/view.rs | 87 ++++++------- src/gui/fields.rs | 2 +- 9 files changed, 482 insertions(+), 54 deletions(-) create mode 100644 src/daemon/overlay/glyph.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a0e9e34..d404d52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - Changed the app and tray icon to a circular design - Fixed source installs failing on a fresh dependency resolution +### Overlay + +- Replaced the M and D text badges with crossed-mic and crossed-headphones glyphs after each name, red for server-set and grey for self-set mute or deafen + ## 0.3.0 (2026-09-03) ### Platforms diff --git a/CONTEXT.md b/CONTEXT.md index 48674a9..f87aab5 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -52,9 +52,12 @@ Platform mechanics live behind ports in `src/platform/`; see name and own user id. Persisted to the roster cache only while connected; speaking state is never persisted (it would be stale on load). - **Roster row** — one participant entry rendered on the overlay surface: - avatar plus username, decorated by speaking ring and mute badges. The - overlay shows only roster rows — never connection or status text. An - empty roster renders an empty transparent surface. + avatar plus username, decorated by the speaking ring and, when the + participant is silenced, by one mute glyph shown inline after the username + (crossed microphone, or crossed headphones when deafened). Red marks a + server-set state, grey a self-set one. The overlay shows only roster rows — + never connection or status text. An empty roster renders an empty + transparent surface. - **RosterChange** — `Changed`/`Unchanged` result of applying a Discord event to the `Overlay`; drives cache writes and view refreshes. - **Overlay layer** — the Wayland layer-shell layer the overlay binds to. diff --git a/Cargo.lock b/Cargo.lock index 24232a7..6dcda8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -595,6 +595,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.8" @@ -792,6 +798,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + [[package]] name = "deranged" version = "0.5.8" @@ -1023,6 +1035,12 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + [[package]] name = "fnv" version = "1.0.7" @@ -1321,6 +1339,16 @@ dependencies = [ "r-efi 6.0.0", ] +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gio" version = "0.18.4" @@ -1881,8 +1909,9 @@ dependencies = [ "cosmic-text", "iced_debug", "iced_graphics", - "kurbo", + "kurbo 0.10.4", "log", + "resvg", "rustc-hash 2.1.3", "softbuffer", "tiny-skia", @@ -1903,6 +1932,7 @@ dependencies = [ "iced_debug", "iced_graphics", "log", + "resvg", "rustc-hash 2.1.3", "thiserror 2.0.20", "wgpu", @@ -2060,9 +2090,25 @@ dependencies = [ "byteorder-lite", "moxcms", "num-traits", - "png", + "png 0.18.1", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", ] +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + [[package]] name = "indexmap" version = "2.14.1" @@ -2243,6 +2289,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + [[package]] name = "layershellev" version = "0.19.1" @@ -2556,7 +2613,7 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation 0.3.2", "once_cell", - "png", + "png 0.18.1", "thiserror 2.0.20", "windows-sys 0.61.2", ] @@ -3177,6 +3234,12 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" version = "1.1.13" @@ -3232,6 +3295,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + [[package]] name = "png" version = "0.18.1" @@ -3380,6 +3456,12 @@ version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.41.0" @@ -3517,6 +3599,32 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", + "zune-jpeg", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.14" @@ -3625,6 +3733,24 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3790,6 +3916,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "skrifa" version = "0.37.0" @@ -3980,6 +4121,9 @@ name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] [[package]] name = "strsim" @@ -3999,6 +4143,16 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.3", + "siphasher", +] + [[package]] name = "swash" version = "0.2.10" @@ -4199,6 +4353,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", + "png 0.17.16", "tiny-skia-path", ] @@ -4466,7 +4621,7 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation 0.3.2", "once_cell", - "png", + "png 0.18.1", "thiserror 2.0.20", "windows-sys 0.61.2", ] @@ -4497,6 +4652,18 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -4509,6 +4676,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-script" version = "0.5.8" @@ -4521,6 +4694,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + [[package]] name = "unicode-width" version = "0.2.2" @@ -4563,6 +4742,33 @@ dependencies = [ "serde", ] +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4881,6 +5087,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "wgpu" version = "27.0.1" @@ -5536,6 +5748,12 @@ version = "0.8.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + [[package]] name = "yazi" version = "0.2.1" @@ -5734,6 +5952,21 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.15.0" diff --git a/Cargo.toml b/Cargo.toml index 18dd3ac..156503b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ iced = { version = "0.14", default-features = false, features = [ "tiny-skia", "crisp", "image-without-codecs", + "svg", "tokio", ] } iced_layershell = "0.19.1" diff --git a/docs/dev/code-layout.md b/docs/dev/code-layout.md index 403b1c3..0bd59f7 100644 --- a/docs/dev/code-layout.md +++ b/docs/dev/code-layout.md @@ -55,6 +55,7 @@ boundary. | `src/daemon/overlay/state.rs` | `Overlay` model methods (`desired_size`, `displayed`, `apply_discord`) | Roster filtering, sizing, avatar cache/dedup | | `src/daemon/overlay/geometry.rs` | `anchor/margin/drag(cfg, …)` | All screen-placement math | | `src/daemon/overlay/view.rs` | `view(&Overlay)` | Widget construction only | +| `src/daemon/overlay/glyph.rs` | `mark_of(&Participant) -> Option` | Mute/deafen glyph mapping for roster rows, free of widgets | | `src/daemon/adapters/discord.rs` | `run(sender, auth) -> DiscordEvent` | Local IPC protocol over `IpcStream`, OAuth token exchange, reconnection, voice subscriptions (Adapter to external Discord API) | | `src/daemon/adapters/ipc.rs` | transport-agnostic `IpcStream` + `DiscordTransport` port | Discord's local IPC wire format: 8-byte LE header, handshake, PING/PONG; per-OS discovery + connect (unix socket / named pipe) behind the package-local `DiscordTransport` port (Adapter) | | `src/daemon/adapters/auth.rs` | `detect() -> Option`, `exchange(code)` | Credential resolution (env → auth.json) and the OAuth code exchange | diff --git a/src/daemon/overlay/glyph.rs b/src/daemon/overlay/glyph.rs new file mode 100644 index 0000000..e625c18 --- /dev/null +++ b/src/daemon/overlay/glyph.rs @@ -0,0 +1,190 @@ +//! The mute/deafen mark: which glyph a roster row shows and in which color. +//! +//! The decision is kept here, free of widgets, so the pins below hold no +//! matter how the row is drawn. + +use iced::Color; + +use crate::daemon::adapters::discord::Participant; + +/// Icon path data from the Material Design Icons set (Pictogrammers Free +/// License, Apache-2.0): the crossed-out microphone and crossed-out +/// headphones. Copied as path data only, in a 24×24 viewBox. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Glyph { + MicOff, + HeadphonesOff, +} + +const MIC_OFF: &str = r#""#; + +const HEADPHONES_OFF: &str = r#""#; + +impl Glyph { + pub(crate) fn svg(self) -> &'static str { + match self { + Self::MicOff => MIC_OFF, + Self::HeadphonesOff => HEADPHONES_OFF, + } + } +} + +/// Tailwind `red-600`: a moderator silenced this participant. +const SERVER_COLOR: Color = Color::from_rgb8(0xDC, 0x26, 0x26); +/// Tailwind `neutral-400`: they silenced themselves. +const SELF_COLOR: Color = Color::from_rgb8(0xA3, 0xA3, 0xA3); + +/// The one badge a row carries, if it carries one at all. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Mark { + pub glyph: Glyph, + pub server_caused: bool, +} + +impl Mark { + pub(crate) fn color(self) -> Color { + if self.server_caused { + SERVER_COLOR + } else { + SELF_COLOR + } + } +} + +/// The mark a participant earns, or none when they are clean. Deafening +/// wins over muting: one badge per row, never two. Server-set flags decide +/// the color on their own, so a self-deafened participant who is also +/// server-muted still reads red. +pub(crate) fn mark_of(p: &Participant) -> Option { + let glyph = if p.deafened() { + Glyph::HeadphonesOff + } else if p.muted() { + Glyph::MicOff + } else { + return None; + }; + Some(Mark { + glyph, + server_caused: p.server_mute || p.server_deaf, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn state( + self_mute: bool, + self_deaf: bool, + server_mute: bool, + server_deaf: bool, + ) -> Participant { + Participant { + id: "42".into(), + name: "fazuh".into(), + avatar_hash: None, + speaking: false, + self_mute, + self_deaf, + server_mute, + server_deaf, + } + } + + /// `(self_mute, self_deaf, server_mute, server_deaf)` and the mark (glyph + /// + server-caused) it must produce, if any. + type Case = ((bool, bool, bool, bool), Option<(Glyph, bool)>); + + const CASES: [Case; 16] = [ + ((false, false, false, false), None), + ((true, false, false, false), Some((Glyph::MicOff, false))), + ( + (false, true, false, false), + Some((Glyph::HeadphonesOff, false)), + ), + ( + (true, true, false, false), + Some((Glyph::HeadphonesOff, false)), + ), + ((false, false, true, false), Some((Glyph::MicOff, true))), + ((true, false, true, false), Some((Glyph::MicOff, true))), + ( + (false, true, true, false), + Some((Glyph::HeadphonesOff, true)), + ), + ( + (true, true, true, false), + Some((Glyph::HeadphonesOff, true)), + ), + ( + (false, false, false, true), + Some((Glyph::HeadphonesOff, true)), + ), + ( + (true, false, false, true), + Some((Glyph::HeadphonesOff, true)), + ), + ( + (false, true, false, true), + Some((Glyph::HeadphonesOff, true)), + ), + ( + (true, true, false, true), + Some((Glyph::HeadphonesOff, true)), + ), + ( + (false, false, true, true), + Some((Glyph::HeadphonesOff, true)), + ), + ( + (true, false, true, true), + Some((Glyph::HeadphonesOff, true)), + ), + ( + (false, true, true, true), + Some((Glyph::HeadphonesOff, true)), + ), + ((true, true, true, true), Some((Glyph::HeadphonesOff, true))), + ]; + + #[test] + fn mark_of_maps_every_mute_flag_combination_to_one_glyph() { + for ((sm, sd, vm, vd), expected) in CASES { + let got = mark_of(&state(sm, sd, vm, vd)).map(|m| (m.glyph, m.server_caused)); + assert_eq!( + got, expected, + "self_mute={sm} self_deaf={sd} server_mute={vm} server_deaf={vd}" + ); + } + } + + #[test] + fn clean_participant_gets_no_mark() { + assert_eq!(mark_of(&state(false, false, false, false)), None); + } + + #[test] + fn server_color_is_red_600_and_self_color_is_neutral_400() { + let red = Mark { + glyph: Glyph::MicOff, + server_caused: true, + } + .color(); + let grey = Mark { + glyph: Glyph::MicOff, + server_caused: false, + } + .color(); + assert_eq!(red, Color::from_rgb8(0xDC, 0x26, 0x26)); + assert_eq!(grey, Color::from_rgb8(0xA3, 0xA3, 0xA3)); + } + + #[test] + fn both_glyphs_are_24by24_viewbox_paths() { + for glyph in [Glyph::MicOff, Glyph::HeadphonesOff] { + let doc = glyph.svg(); + assert!(doc.starts_with(r#""#), "{doc}"); + assert!(doc.contains(" Color { Color::from_rgb8(r, g, b) } -const MUTE_RED: Color = Color::from_rgb(0.95, 0.25, 0.26); -const DEAF_ORANGE: Color = Color::from_rgb(0.96, 0.72, 0.24); +/// Glyph drawn inline after the username, logical px. +const INLINE_SIZE: u32 = 16; const FALLBACK_COLORS: [Color; 6] = [ Color::from_rgb(0.36, 0.44, 0.96), @@ -104,6 +108,8 @@ fn participant_row<'a, M: 'static>( }) .into(); + let mark = mark_of(p); + let name = truncate(&p.name, state.config().max_username_length); // Speakers are differentiated by the ring only — names stay fully // opaque so the per-part sliders are the only transparency knobs. @@ -133,33 +139,50 @@ fn participant_row<'a, M: 'static>( }) .into(); - let mut badges = row![].spacing(4.0); - if p.muted() { - badges = badges.push(badge("M", MUTE_RED, alphas.text, alphas.box_bg, state)); - } - if p.deafened() { - badges = badges.push(badge("D", DEAF_ORANGE, alphas.text, alphas.box_bg, state)); - } + // The mark, inline right after the username. + let inline = + mark.map(|mark| glyph(mark.glyph, scaled(state, INLINE_SIZE), alpha(mark, alphas))); if state.config().rtl { // Avatar on the right, name to its left, text right-aligned. - row![ + let mut row = row![ Space::new().width(Length::Fill), - name_right_aligned(state, name_area), - badges, - avatar + name_right_aligned(state, name_area) ] .spacing(8.0) - .align_y(iced::Alignment::Center) - .into() + .align_y(iced::Alignment::Center); + if let Some(inline) = inline { + row = row.push(inline); + } + row.push(avatar).into() } else { - row![avatar, name_area, Space::new().width(Length::Fill), badges] + let mut row = row![avatar, name_area] .spacing(8.0) - .align_y(iced::Alignment::Center) - .into() + .align_y(iced::Alignment::Center); + if let Some(inline) = inline { + row = row.push(inline); + } + row.push(Space::new().width(Length::Fill)).into() } } +/// The mark's color, dimmed by the text-opacity knob like any row content. +fn alpha(mark: Mark, alphas: Alphas) -> Color { + Color { + a: alphas.text, + ..mark.color() + } +} + +/// Vector glyph of one MDI icon, painted flat in `color`. +fn glyph(glyph: Glyph, px: f32, color: Color) -> Element<'static, M> { + svg::Svg::new(svg::Handle::from_memory(glyph.svg().as_bytes())) + .width(Length::Fixed(px)) + .height(Length::Fixed(px)) + .style(move |_theme, _status| svg::Style { color: Some(color) }) + .into() +} + /// In RTL mode the name hugs the avatar: right-aligned inside a filling row. fn name_right_aligned<'a, M: 'a>(state: &Overlay, name_area: Element<'a, M>) -> Element<'a, M> { let text_size = scaled(state, state.config().text_size); @@ -173,34 +196,6 @@ fn name_right_aligned<'a, M: 'a>(state: &Overlay, name_area: Element<'a, M>) -> .into() } -fn badge( - label: &str, - color: Color, - alpha: f32, - bg_alpha: f32, - state: &Overlay, -) -> Element<'static, M> { - let size = scaled(state, state.config().text_size) * 0.7; - let bg = Color { - a: bg_alpha, - ..color - }; - container(text(label.to_string()).size(size).color(Color { - a: alpha, - ..Color::WHITE - })) - .padding(2.0) - .style(move |_t| ContainerStyle { - background: Some(bg.into()), - border: Border { - radius: (size * 0.5).into(), - ..Border::default() - }, - ..ContainerStyle::default() - }) - .into() -} - fn fallback_avatar(id: &str, name: &str, px: f32, alpha: f32) -> Element<'static, M> { let color = FALLBACK_COLORS[id.bytes().map(usize::from).sum::() % FALLBACK_COLORS.len()]; let bg = Color { a: alpha, ..color }; diff --git a/src/gui/fields.rs b/src/gui/fields.rs index b0469b4..e57438b 100644 --- a/src/gui/fields.rs +++ b/src/gui/fields.rs @@ -247,7 +247,7 @@ pub(super) const FIELDS: &[Field] = &[ Field { section: Section::Opacity, label: "overall", - tip: "Dims everything together: avatars, usernames, badges and the speaking ring.", + tip: "Dims everything together: avatars, usernames, glyphs and the speaking ring.", render: f_opacity, }, Field { From bd4a87bedbfc7768063a59b3cce4299c0c4db081 Mon Sep 17 00:00:00 2001 From: FAZuH Date: Sat, 12 Sep 2026 01:20:21 +0700 Subject: [PATCH 2/2] docs: sync changelog with released 0.3.1 section The v0.3.2 line was cut from the pre-release tip, so its CHANGELOG still listed the 0.3.1 entries under [Unreleased] and lacked the materialized 0.3.1 section. Release previews counted the shipped entries twice. --- CHANGELOG.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d404d52..2a9a2b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,17 @@ ## [Unreleased] +### Overlay + +- Replaced the M and D text badges with crossed-mic and crossed-headphones glyphs after each name, red for server-set and grey for self-set mute or deafen + +## 0.3.1 (2026-09-11) + ### Platforms - Changed the app and tray icon to a circular design - Fixed source installs failing on a fresh dependency resolution -### Overlay - -- Replaced the M and D text badges with crossed-mic and crossed-headphones glyphs after each name, red for server-set and grey for self-set mute or deafen - ## 0.3.0 (2026-09-03) ### Platforms