Will change this commit message to mean something later
Some checks failed
dev release / build (push) Failing after 2s
Some checks failed
dev release / build (push) Failing after 2s
This commit is contained in:
parent
905d91580d
commit
175af5d483
7 changed files with 611 additions and 28 deletions
105
Cargo.lock
generated
105
Cargo.lock
generated
|
|
@ -100,6 +100,16 @@ version = "2.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bread-shared"
|
||||||
|
version = "0.7.0"
|
||||||
|
dependencies = [
|
||||||
|
"dirs",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"toml 0.8.23",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bread-theme"
|
name = "bread-theme"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
|
@ -111,11 +121,23 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bread-utils"
|
||||||
|
version = "0.3.1"
|
||||||
|
dependencies = [
|
||||||
|
"bread-shared",
|
||||||
|
"dirs",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "breadbar"
|
name = "breadbar"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bread-shared",
|
||||||
"bread-theme",
|
"bread-theme",
|
||||||
|
"bread-utils",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"gtk4-layer-shell",
|
"gtk4-layer-shell",
|
||||||
|
|
@ -1115,7 +1137,7 @@ version = "3.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"toml_edit",
|
"toml_edit 0.25.13+spec-1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1316,6 +1338,15 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
|
@ -1426,7 +1457,7 @@ dependencies = [
|
||||||
"cfg-expr",
|
"cfg-expr",
|
||||||
"heck",
|
"heck",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"toml",
|
"toml 1.1.3+spec-1.1.0",
|
||||||
"version-compare",
|
"version-compare",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1523,6 +1554,18 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned 0.6.9",
|
||||||
|
"toml_datetime 0.6.11",
|
||||||
|
"toml_edit 0.22.27",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.1.3+spec-1.1.0"
|
version = "1.1.3+spec-1.1.0"
|
||||||
|
|
@ -1531,11 +1574,20 @@ checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_spanned",
|
"serde_spanned 1.1.1",
|
||||||
"toml_datetime",
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"toml_writer",
|
"toml_writer",
|
||||||
"winnow",
|
"winnow 1.0.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1547,6 +1599,20 @@ dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned 0.6.9",
|
||||||
|
"toml_datetime 0.6.11",
|
||||||
|
"toml_write",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.25.13+spec-1.1.0"
|
version = "0.25.13+spec-1.1.0"
|
||||||
|
|
@ -1554,9 +1620,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
|
checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime",
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow",
|
"winnow 1.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1565,9 +1631,15 @@ version = "1.1.2+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow 1.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_writer"
|
||||||
version = "1.1.2+spec-1.1.0"
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
|
@ -1814,6 +1886,15 @@ version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|
@ -1865,7 +1946,7 @@ dependencies = [
|
||||||
"uds_windows",
|
"uds_windows",
|
||||||
"uuid",
|
"uuid",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
"winnow",
|
"winnow 1.0.4",
|
||||||
"zbus_macros",
|
"zbus_macros",
|
||||||
"zbus_names",
|
"zbus_names",
|
||||||
"zvariant",
|
"zvariant",
|
||||||
|
|
@ -1893,7 +1974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
|
checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"winnow",
|
"winnow 1.0.4",
|
||||||
"zvariant",
|
"zvariant",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1912,7 +1993,7 @@ dependencies = [
|
||||||
"endi",
|
"endi",
|
||||||
"enumflags2",
|
"enumflags2",
|
||||||
"serde",
|
"serde",
|
||||||
"winnow",
|
"winnow 1.0.4",
|
||||||
"zvariant_derive",
|
"zvariant_derive",
|
||||||
"zvariant_utils",
|
"zvariant_utils",
|
||||||
]
|
]
|
||||||
|
|
@ -1940,5 +2021,5 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"syn",
|
"syn",
|
||||||
"winnow",
|
"winnow 1.0.4",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
15
Cargo.toml
15
Cargo.toml
|
|
@ -11,6 +11,12 @@ categories = ["gui"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.10", features = ["gtk"] }
|
bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.10", features = ["gtk"] }
|
||||||
|
# Widget rendering client: bread-utils::BreadClient (emit/request/subscribe)
|
||||||
|
# for talking to breadd's IPC socket, and bread-shared purely for the
|
||||||
|
# WidgetSpec/WidgetNode wire types so we deserialize into real structs
|
||||||
|
# instead of hand-parsing serde_json::Value. See src/widgets/.
|
||||||
|
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.3.1", features = ["bread-client"] }
|
||||||
|
bread-shared = { git = "https://git.breadway.dev/Breadway/bread", tag = "v0.7.0" }
|
||||||
gtk4 = { version = "0.11", features = ["v4_12"] }
|
gtk4 = { version = "0.11", features = ["v4_12"] }
|
||||||
gtk4-layer-shell = "0.8"
|
gtk4-layer-shell = "0.8"
|
||||||
relm4 = { version = "0.11", features = ["macros"] }
|
relm4 = { version = "0.11", features = ["macros"] }
|
||||||
|
|
@ -24,6 +30,15 @@ serde_json = "1"
|
||||||
# are vector-only). Needed because librsvg dropped its gdk-pixbuf SVG loader.
|
# are vector-only). Needed because librsvg dropped its gdk-pixbuf SVG loader.
|
||||||
resvg = { version = "0.44", default-features = false }
|
resvg = { version = "0.44", default-features = false }
|
||||||
|
|
||||||
|
# TEMPORARY local-dev override: the widget feature's bread-shared/bread-utils
|
||||||
|
# changes aren't tagged/released yet, so point the git deps above at the
|
||||||
|
# local checkouts instead. Remove this section (and bump the tags above)
|
||||||
|
# once bread-ecosystem and bread have real release tags for this work.
|
||||||
|
[patch."https://git.breadway.dev/Breadway/bread-ecosystem"]
|
||||||
|
bread-utils = { path = "../bread-ecosystem/bread-utils" }
|
||||||
|
[patch."https://git.breadway.dev/Breadway/bread"]
|
||||||
|
bread-shared = { path = "../bread/bread-shared" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
|
||||||
142
src/main.rs
142
src/main.rs
|
|
@ -8,6 +8,7 @@ mod bar;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
mod osd;
|
mod osd;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
mod widgets;
|
||||||
|
|
||||||
/// Thresholds above which the bar's CPU/RAM/power-draw readouts appear at
|
/// Thresholds above which the bar's CPU/RAM/power-draw readouts appear at
|
||||||
/// all — see `AppInput::StatsUpdate`. Below these, the bar stays quiet.
|
/// all — see `AppInput::StatsUpdate`. Below these, the bar stays quiet.
|
||||||
|
|
@ -94,6 +95,15 @@ pub struct App {
|
||||||
tray_sep: gtk4::Separator,
|
tray_sep: gtk4::Separator,
|
||||||
tray_box: gtk4::Box,
|
tray_box: gtk4::Box,
|
||||||
tray_items: std::collections::HashMap<String, gtk4::Button>,
|
tray_items: std::collections::HashMap<String, gtk4::Button>,
|
||||||
|
|
||||||
|
// ── Lua-declared widgets ─────────────────────────────────────────────
|
||||||
|
// One container per WidgetPlacement (see bread_shared::widget), fully
|
||||||
|
// rebuilt on every AppInput::WidgetsUpdate — see widgets::client's
|
||||||
|
// module doc for why that's simpler than incremental patching here.
|
||||||
|
widget_containers:
|
||||||
|
std::collections::HashMap<bread_shared::widget::WidgetPlacement, gtk4::Box>,
|
||||||
|
widget_tray_section: gtk4::Box,
|
||||||
|
widget_tray_sep: gtk4::Separator,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -109,6 +119,7 @@ pub enum AppInput {
|
||||||
BtPopoverData(bar::bluetooth::BtPopoverData),
|
BtPopoverData(bar::bluetooth::BtPopoverData),
|
||||||
MediaUpdate(bar::media::MediaState),
|
MediaUpdate(bar::media::MediaState),
|
||||||
ControlPanelData(bar::control::ControlPanelData),
|
ControlPanelData(bar::control::ControlPanelData),
|
||||||
|
WidgetsUpdate(Vec<bread_shared::widget::WidgetSpec>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[relm4::component(pub)]
|
#[relm4::component(pub)]
|
||||||
|
|
@ -125,17 +136,6 @@ impl SimpleComponent for App {
|
||||||
|
|
||||||
#[name = "center_box"]
|
#[name = "center_box"]
|
||||||
gtk::CenterBox {
|
gtk::CenterBox {
|
||||||
#[wrap(Some)]
|
|
||||||
set_start_widget = >k::Box {
|
|
||||||
set_orientation: gtk::Orientation::Horizontal,
|
|
||||||
set_spacing: 0,
|
|
||||||
|
|
||||||
#[name = "workspace_box"]
|
|
||||||
gtk::Box {
|
|
||||||
set_orientation: gtk::Orientation::Horizontal,
|
|
||||||
set_spacing: 4,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -153,6 +153,32 @@ impl SimpleComponent for App {
|
||||||
root.set_anchor(Edge::Right, true);
|
root.set_anchor(Edge::Right, true);
|
||||||
root.set_exclusive_zone(32);
|
root.set_exclusive_zone(32);
|
||||||
|
|
||||||
|
// ── Workspace row (left) ────────────────────────────────────────
|
||||||
|
// Built imperatively (not via the view! macro) so a widget
|
||||||
|
// container can sit as a plain sibling of workspace_box — see
|
||||||
|
// WidgetPlacement::RightOfWorkspaces below.
|
||||||
|
let workspace_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 4);
|
||||||
|
let workspace_row = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
|
||||||
|
workspace_row.append(&workspace_box);
|
||||||
|
|
||||||
|
// ── Lua-declared widget containers ──────────────────────────────
|
||||||
|
// One per WidgetPlacement; positioned into the layout below as each
|
||||||
|
// surrounding section (workspace row / center area / stats box /
|
||||||
|
// control popover) is built. Populated by widgets::client's
|
||||||
|
// events.subscribe-driven refresh loop, started at the end of init.
|
||||||
|
use bread_shared::widget::WidgetPlacement;
|
||||||
|
let widget_right_of_workspaces = gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
|
||||||
|
widget_right_of_workspaces.add_css_class("bread-widget-slot");
|
||||||
|
workspace_row.append(&widget_right_of_workspaces);
|
||||||
|
|
||||||
|
let widget_left_of_clock = gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
|
||||||
|
widget_left_of_clock.add_css_class("bread-widget-slot");
|
||||||
|
let widget_right_of_clock = gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
|
||||||
|
widget_right_of_clock.add_css_class("bread-widget-slot");
|
||||||
|
|
||||||
|
let widget_left_of_stats = gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
|
||||||
|
widget_left_of_stats.add_css_class("bread-widget-slot");
|
||||||
|
|
||||||
// ── SVG icon sets ────────────────────────────────────────────────
|
// ── SVG icon sets ────────────────────────────────────────────────
|
||||||
use bar::stats::{
|
use bar::stats::{
|
||||||
AC_POWER, BAT_HIGH, BAT_LOW, BAT_MID, BT_CONNECTED, BT_OFF, BT_ON, WIFI_MEDIUM,
|
AC_POWER, BAT_HIGH, BAT_LOW, BAT_MID, BT_CONNECTED, BT_OFF, BT_ON, WIFI_MEDIUM,
|
||||||
|
|
@ -278,15 +304,18 @@ impl SimpleComponent for App {
|
||||||
let clock_lbl = gtk4::Label::new(Some(&bar::clock::current()));
|
let clock_lbl = gtk4::Label::new(Some(&bar::clock::current()));
|
||||||
clock_lbl.add_css_class("clock-label");
|
clock_lbl.add_css_class("clock-label");
|
||||||
|
|
||||||
// Center area: [media_widget · clock]
|
// Center area: [media_widget · widgets · clock · widgets]
|
||||||
let center_area = gtk4::Box::new(gtk4::Orientation::Horizontal, 10);
|
let center_area = gtk4::Box::new(gtk4::Orientation::Horizontal, 10);
|
||||||
center_area.add_css_class("center-area");
|
center_area.add_css_class("center-area");
|
||||||
center_area.append(&media_widget);
|
center_area.append(&media_widget);
|
||||||
|
center_area.append(&widget_left_of_clock);
|
||||||
center_area.append(&clock_lbl);
|
center_area.append(&clock_lbl);
|
||||||
|
center_area.append(&widget_right_of_clock);
|
||||||
|
|
||||||
// ── Stats box (right side) ───────────────────────────────────────
|
// ── Stats box (right side) ───────────────────────────────────────
|
||||||
let stats_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
|
let stats_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
|
||||||
stats_box.add_css_class("stats-box");
|
stats_box.add_css_class("stats-box");
|
||||||
|
stats_box.append(&widget_left_of_stats);
|
||||||
|
|
||||||
// CPU/RAM/power draw: hidden by default (see StatsUpdate), so this
|
// CPU/RAM/power draw: hidden by default (see StatsUpdate), so this
|
||||||
// whole sub-group — plus its separator — collapses away when quiet.
|
// whole sub-group — plus its separator — collapses away when quiet.
|
||||||
|
|
@ -509,6 +538,24 @@ impl SimpleComponent for App {
|
||||||
tray_sep.set_visible(false);
|
tray_sep.set_visible(false);
|
||||||
panel_inner.append(&tray_sep);
|
panel_inner.append(&tray_sep);
|
||||||
|
|
||||||
|
// Widgets section — Lua-declared widgets with placement = "tray".
|
||||||
|
// Same collapse-when-empty idiom as the Apps section above.
|
||||||
|
let widget_tray_section = gtk4::Box::new(gtk4::Orientation::Vertical, 4);
|
||||||
|
widget_tray_section.add_css_class("control-panel-section");
|
||||||
|
let widget_tray_header = gtk4::Label::new(Some("Widgets"));
|
||||||
|
widget_tray_header.add_css_class("control-panel-section-header");
|
||||||
|
widget_tray_header.set_xalign(0.0);
|
||||||
|
let widget_tray_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
|
||||||
|
widget_tray_box.add_css_class("tray-box");
|
||||||
|
widget_tray_section.append(&widget_tray_header);
|
||||||
|
widget_tray_section.append(&widget_tray_box);
|
||||||
|
widget_tray_section.set_visible(false);
|
||||||
|
panel_inner.append(&widget_tray_section);
|
||||||
|
|
||||||
|
let widget_tray_sep = gtk4::Separator::new(gtk4::Orientation::Horizontal);
|
||||||
|
widget_tray_sep.set_visible(false);
|
||||||
|
panel_inner.append(&widget_tray_sep);
|
||||||
|
|
||||||
// Power section
|
// Power section
|
||||||
let power_section = gtk4::Box::new(gtk4::Orientation::Vertical, 4);
|
let power_section = gtk4::Box::new(gtk4::Orientation::Vertical, 4);
|
||||||
power_section.add_css_class("control-panel-section");
|
power_section.add_css_class("control-panel-section");
|
||||||
|
|
@ -583,15 +630,24 @@ impl SimpleComponent for App {
|
||||||
|
|
||||||
stats_box.append(&hamburger_btn);
|
stats_box.append(&hamburger_btn);
|
||||||
|
|
||||||
|
let widget_containers = std::collections::HashMap::from([
|
||||||
|
(WidgetPlacement::RightOfWorkspaces, widget_right_of_workspaces),
|
||||||
|
(WidgetPlacement::LeftOfClock, widget_left_of_clock),
|
||||||
|
(WidgetPlacement::RightOfClock, widget_right_of_clock),
|
||||||
|
(WidgetPlacement::LeftOfStats, widget_left_of_stats),
|
||||||
|
(WidgetPlacement::Tray, widget_tray_box),
|
||||||
|
]);
|
||||||
|
|
||||||
// ── Assemble ─────────────────────────────────────────────────────
|
// ── Assemble ─────────────────────────────────────────────────────
|
||||||
let widgets = view_output!();
|
let widgets = view_output!();
|
||||||
|
widgets.center_box.set_start_widget(Some(&workspace_row));
|
||||||
widgets.center_box.set_center_widget(Some(¢er_area));
|
widgets.center_box.set_center_widget(Some(¢er_area));
|
||||||
widgets.center_box.set_end_widget(Some(&stats_box));
|
widgets.center_box.set_end_widget(Some(&stats_box));
|
||||||
|
|
||||||
let mut model = App {
|
let model = App {
|
||||||
workspaces: vec![],
|
workspaces: vec![],
|
||||||
active_ws: 1,
|
active_ws: 1,
|
||||||
workspace_box: gtk4::Box::new(gtk4::Orientation::Horizontal, 4),
|
workspace_box,
|
||||||
button_map: std::collections::HashMap::new(),
|
button_map: std::collections::HashMap::new(),
|
||||||
time_str: bar::clock::current(),
|
time_str: bar::clock::current(),
|
||||||
clock_lbl,
|
clock_lbl,
|
||||||
|
|
@ -641,8 +697,10 @@ impl SimpleComponent for App {
|
||||||
tray_sep,
|
tray_sep,
|
||||||
tray_box,
|
tray_box,
|
||||||
tray_items: std::collections::HashMap::new(),
|
tray_items: std::collections::HashMap::new(),
|
||||||
|
widget_containers,
|
||||||
|
widget_tray_section,
|
||||||
|
widget_tray_sep,
|
||||||
};
|
};
|
||||||
model.workspace_box = widgets.workspace_box.clone();
|
|
||||||
|
|
||||||
theme::apply();
|
theme::apply();
|
||||||
bar::workspaces::spawn_watcher(sender.clone());
|
bar::workspaces::spawn_watcher(sender.clone());
|
||||||
|
|
@ -651,6 +709,7 @@ impl SimpleComponent for App {
|
||||||
bar::tray::spawn_watcher(sender.clone());
|
bar::tray::spawn_watcher(sender.clone());
|
||||||
bar::wifi::spawn_status_poller(sender.clone());
|
bar::wifi::spawn_status_poller(sender.clone());
|
||||||
bar::media::spawn_poller(sender.clone());
|
bar::media::spawn_poller(sender.clone());
|
||||||
|
widgets::client::spawn(sender.clone());
|
||||||
notifications::spawn();
|
notifications::spawn();
|
||||||
osd::spawn();
|
osd::spawn();
|
||||||
|
|
||||||
|
|
@ -872,11 +931,64 @@ impl SimpleComponent for App {
|
||||||
});
|
});
|
||||||
self.panel_sink_signal = Some(id);
|
self.panel_sink_signal = Some(id);
|
||||||
}
|
}
|
||||||
|
AppInput::WidgetsUpdate(specs) => {
|
||||||
|
self.reconcile_widgets(specs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
fn reconcile_widgets(&mut self, specs: Vec<bread_shared::widget::WidgetSpec>) {
|
||||||
|
for container in self.widget_containers.values() {
|
||||||
|
while let Some(child) = container.first_child() {
|
||||||
|
container.remove(&child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut by_placement: std::collections::HashMap<
|
||||||
|
bread_shared::widget::WidgetPlacement,
|
||||||
|
Vec<&bread_shared::widget::WidgetSpec>,
|
||||||
|
> = std::collections::HashMap::new();
|
||||||
|
for spec in &specs {
|
||||||
|
by_placement.entry(spec.placement).or_default().push(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (placement, mut group) in by_placement {
|
||||||
|
let Some(container) = self.widget_containers.get(&placement) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
group.sort_by_key(|s| s.order);
|
||||||
|
for spec in group {
|
||||||
|
if !spec.visible {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let node = widgets::build_node(&spec.root, &spec.id);
|
||||||
|
if let Some(tooltip) = &spec.tooltip {
|
||||||
|
node.set_tooltip_text(Some(tooltip));
|
||||||
|
}
|
||||||
|
container.append(&node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Tray placement has its own section/separator (handled below,
|
||||||
|
// same as the existing SNI tray items) — an empty inline slot has no
|
||||||
|
// such wrapper, so it must hide itself to stop contributing to
|
||||||
|
// center_area's `spacing` gap.
|
||||||
|
for (placement, container) in &self.widget_containers {
|
||||||
|
if *placement == bread_shared::widget::WidgetPlacement::Tray {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
container.set_visible(container.first_child().is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_tray_widgets = specs.iter().any(|s| {
|
||||||
|
s.visible && s.placement == bread_shared::widget::WidgetPlacement::Tray
|
||||||
|
});
|
||||||
|
self.widget_tray_section.set_visible(has_tray_widgets);
|
||||||
|
self.widget_tray_sep.set_visible(has_tray_widgets);
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_wifi_label(&self) {
|
fn apply_wifi_label(&self) {
|
||||||
let label = match &self.wifi_profile {
|
let label = match &self.wifi_profile {
|
||||||
Some(p) => format!("{p} · {}", self.current_ssid),
|
Some(p) => format!("{p} · {}", self.current_ssid),
|
||||||
|
|
|
||||||
52
src/theme.rs
52
src/theme.rs
|
|
@ -89,7 +89,57 @@ fn load_css() -> String {
|
||||||
.control-panel-sink-dropdown {{ }}\
|
.control-panel-sink-dropdown {{ }}\
|
||||||
.power-row {{ margin-top: 2px; }}\
|
.power-row {{ margin-top: 2px; }}\
|
||||||
.power-btn {{ min-width: 40px; padding: 8px; border-radius: {radius_sm}; }}\
|
.power-btn {{ min-width: 40px; padding: 8px; border-radius: {radius_sm}; }}\
|
||||||
separator {{ margin: 4px 0; }}",
|
separator {{ margin: 4px 0; }}\
|
||||||
|
/* Lua-declared widgets (see Documentation.md's Widgets §style): the\
|
||||||
|
slot rule below is what the four inline `.bread-widget-slot`\
|
||||||
|
containers in main.rs rely on for the same 12px stat-pair rhythm\
|
||||||
|
everything else in the bar uses (they carried the class with no\
|
||||||
|
rule defining it until now). Everything after that is the fixed,\
|
||||||
|
closed `style` vocabulary a `WidgetNode` can opt into — one class\
|
||||||
|
per enum variant, so a module can only ever pick from this set,\
|
||||||
|
never inject arbitrary CSS. The progress-bar rules give an\
|
||||||
|
unstyled Progress node an intentional accent-colored fill instead\
|
||||||
|
of Adwaita's default blue-on-gray, and let `style.color` retint\
|
||||||
|
that fill the same way it retints label/icon text. */\
|
||||||
|
.bread-widget-slot {{ margin-right: 12px; }}\
|
||||||
|
progressbar.bread-widget-node trough {{ background-image: none; background-color: alpha(@accent, 0.25); border-radius: 3px; min-height: 6px; }}\
|
||||||
|
progressbar.bread-widget-node trough progress {{ background-image: none; background-color: @accent; border-radius: 3px; min-height: 6px; }}\
|
||||||
|
progressbar.bread-widget-node.bread-color-fg trough progress {{ background-color: @fg; }}\
|
||||||
|
progressbar.bread-widget-node.bread-color-dim trough progress {{ background-color: alpha(@fg, 0.6); }}\
|
||||||
|
progressbar.bread-widget-node.bread-color-accent trough progress {{ background-color: @accent; }}\
|
||||||
|
progressbar.bread-widget-node.bread-color-red trough progress {{ background-color: @red; }}\
|
||||||
|
progressbar.bread-widget-node.bread-color-green trough progress {{ background-color: @green; }}\
|
||||||
|
progressbar.bread-widget-node.bread-color-yellow trough progress {{ background-color: @yellow; }}\
|
||||||
|
progressbar.bread-widget-node.bread-color-blue trough progress {{ background-color: @blue; }}\
|
||||||
|
progressbar.bread-widget-node.bread-color-pink trough progress {{ background-color: @pink; }}\
|
||||||
|
progressbar.bread-widget-node.bread-color-teal trough progress {{ background-color: @teal; }}\
|
||||||
|
.bread-color-fg {{ color: @fg; }}\
|
||||||
|
.bread-color-dim {{ color: @fg; opacity: 0.6; }}\
|
||||||
|
.bread-color-accent {{ color: @accent; }}\
|
||||||
|
.bread-color-red {{ color: @red; }}\
|
||||||
|
.bread-color-green {{ color: @green; }}\
|
||||||
|
.bread-color-yellow {{ color: @yellow; }}\
|
||||||
|
.bread-color-blue {{ color: @blue; }}\
|
||||||
|
.bread-color-pink {{ color: @pink; }}\
|
||||||
|
.bread-color-teal {{ color: @teal; }}\
|
||||||
|
.bread-weight-normal {{ font-weight: normal; }}\
|
||||||
|
.bread-weight-bold {{ font-weight: bold; }}\
|
||||||
|
.bread-size-xs {{ font-size: 10px; }}\
|
||||||
|
.bread-size-sm {{ font-size: 12px; }}\
|
||||||
|
.bread-size-md {{ font-size: 14px; }}\
|
||||||
|
.bread-size-lg {{ font-size: 16px; }}\
|
||||||
|
.bread-size-xl {{ font-size: 20px; }}\
|
||||||
|
.bread-bg-none {{ background-color: transparent; }}\
|
||||||
|
.bread-bg-surface {{ background-color: @surface; color: @on-surface; }}\
|
||||||
|
.bread-bg-card {{ background-color: @surface; color: @on-surface; border-radius: 8px; padding: 12px; }}\
|
||||||
|
.bread-radius-none {{ border-radius: 0; }}\
|
||||||
|
.bread-radius-sm {{ border-radius: 4px; }}\
|
||||||
|
.bread-radius-md {{ border-radius: 8px; }}\
|
||||||
|
.bread-radius-full {{ border-radius: 999px; }}\
|
||||||
|
.bread-padding-none {{ padding: 0; }}\
|
||||||
|
.bread-padding-xs {{ padding: 4px; }}\
|
||||||
|
.bread-padding-sm {{ padding: 8px; }}\
|
||||||
|
.bread-padding-md {{ padding: 12px; }}",
|
||||||
bg_plain = p.background,
|
bg_plain = p.background,
|
||||||
bg_rgba = hex_to_rgba(&p.background, 0.92),
|
bg_rgba = hex_to_rgba(&p.background, 0.92),
|
||||||
surface = p.color0,
|
surface = p.color0,
|
||||||
|
|
|
||||||
106
src/widgets/client.rs
Normal file
106
src/widgets/client.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
//! Connects to breadd's IPC socket and keeps the bar's widget set in sync.
|
||||||
|
//!
|
||||||
|
//! breadbar is level-triggered here, not edge-triggered: `bread.widget.*`
|
||||||
|
//! events are used purely as a "something changed, go re-fetch" signal, not
|
||||||
|
//! applied as incremental patches. Every dirty signal (and the initial
|
||||||
|
//! connect) re-requests the complete widget list and hands it to `update()`
|
||||||
|
//! as one `AppInput::WidgetsUpdate`, which reconciles the bar's containers
|
||||||
|
//! from scratch. This sidesteps event-ordering/drop concerns entirely, and
|
||||||
|
//! widget registries are small enough that re-fetching the full list on
|
||||||
|
//! every change is not a real cost.
|
||||||
|
|
||||||
|
use crate::{App, AppInput};
|
||||||
|
use bread_shared::widget::WidgetSpec;
|
||||||
|
use bread_utils::bread_client::BreadClient;
|
||||||
|
use relm4::ComponentSender;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// breadbar's own registered app id — already reserved in
|
||||||
|
/// `bread_shared::apps::KNOWN_APPS` (see `Documentation.md`'s Namespaces
|
||||||
|
/// section). Used both to fetch widgets and to publish click events.
|
||||||
|
pub const APP_ID: &str = "bar";
|
||||||
|
|
||||||
|
/// Safety-net poll interval. `bread.widget.cleared` (emitted once per
|
||||||
|
/// daemon reload, including a full restart — see breadd's `reload_internal`)
|
||||||
|
/// is meant to catch the case where a module stops registering widgets
|
||||||
|
/// without anything else re-triggering a fetch, but a *restart* (as opposed
|
||||||
|
/// to a live `bread reload`) drops the subscription entirely; if that one
|
||||||
|
/// event fires before `BreadClient::subscribe`'s reconnect-with-backoff
|
||||||
|
/// finishes re-establishing the stream, it's missed and there's no second
|
||||||
|
/// chance from the event side. This poll is the backstop for that race —
|
||||||
|
/// infrequent enough that it's not a real cost, frequent enough that a missed
|
||||||
|
/// event self-heals well within a session rather than needing a manual
|
||||||
|
/// breadbar restart to clear stale widgets.
|
||||||
|
const POLL_INTERVAL: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
/// Start the widget subsystem: an initial fetch, a live subscription that
|
||||||
|
/// re-fetches on every `bread.widget.*` change, and a low-frequency poll as
|
||||||
|
/// a backstop against the reconnect race described above. Call once from
|
||||||
|
/// `init`.
|
||||||
|
pub fn spawn(sender: ComponentSender<App>) {
|
||||||
|
// BreadClient::request is blocking std I/O; run it off the tokio
|
||||||
|
// runtime breadbar's other pollers rely on, same as the reasoning in
|
||||||
|
// `BreadClient::subscribe`'s own background-thread design.
|
||||||
|
let initial = sender.clone();
|
||||||
|
std::thread::spawn(move || fetch_and_send(&initial));
|
||||||
|
|
||||||
|
// `subscribe` already reconnects with backoff on its own background
|
||||||
|
// thread for the lifetime of the process — there is no natural point to
|
||||||
|
// stop it before the app exits, so the handle is intentionally leaked
|
||||||
|
// rather than threaded through App just to be dropped at shutdown.
|
||||||
|
let live = sender.clone();
|
||||||
|
let client = BreadClient::connect(APP_ID);
|
||||||
|
let subscription = client.subscribe("bread.widget.**", move |_event| {
|
||||||
|
fetch_and_send(&live);
|
||||||
|
});
|
||||||
|
std::mem::forget(subscription);
|
||||||
|
|
||||||
|
let polled = sender.clone();
|
||||||
|
relm4::spawn(async move {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(POLL_INTERVAL).await;
|
||||||
|
let polled = polled.clone();
|
||||||
|
std::thread::spawn(move || fetch_and_send(&polled));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_and_send(sender: &ComponentSender<App>) {
|
||||||
|
let client = BreadClient::connect(APP_ID);
|
||||||
|
let Some(result) = client.request("widgets.list", serde_json::Value::Null) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Decode element-wise rather than `Vec<WidgetSpec>` in one shot — one
|
||||||
|
// malformed entry from any module (a bad `class`, an unknown enum value,
|
||||||
|
// ...) must not blank out every other module's widgets.
|
||||||
|
let raw: Vec<serde_json::Value> = serde_json::from_value(result).unwrap_or_default();
|
||||||
|
let specs: Vec<WidgetSpec> = raw
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|v| {
|
||||||
|
// `id`/`module` are read before the value is consumed by the
|
||||||
|
// failed parse below, so a malformed spec still names itself in
|
||||||
|
// the warning instead of just printing a bare serde error.
|
||||||
|
let id = v.get("id").and_then(|x| x.as_str()).unwrap_or("?").to_string();
|
||||||
|
let module = v.get("module").and_then(|x| x.as_str()).unwrap_or("?").to_string();
|
||||||
|
match serde_json::from_value::<WidgetSpec>(v) {
|
||||||
|
Ok(spec) => Some(spec),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"breadbar: dropping malformed widget spec (id={id}, module={module}): {e}"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
sender.input(AppInput::WidgetsUpdate(specs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish a widget click back to breadd. `action` is whatever opaque value
|
||||||
|
/// the Lua module put in the clicked node's `on_click`.
|
||||||
|
pub fn emit_click(widget_id: &str, action: &serde_json::Value) {
|
||||||
|
BreadClient::connect(APP_ID).emit(
|
||||||
|
"bread.bar.widget_clicked",
|
||||||
|
serde_json::json!({ "widget_id": widget_id, "action": action }),
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/widgets/mod.rs
Normal file
7
src/widgets/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
//! Lua-declared, live-updating widgets (see `Documentation.md`'s "Widgets"
|
||||||
|
//! section in the `bread` repo) rendered into breadbar's fixed layout slots.
|
||||||
|
|
||||||
|
pub mod client;
|
||||||
|
mod render;
|
||||||
|
|
||||||
|
pub use render::build_node;
|
||||||
212
src/widgets/render.rs
Normal file
212
src/widgets/render.rs
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
//! Turns a `WidgetNode` tree into a live GTK4 widget tree.
|
||||||
|
//!
|
||||||
|
//! There is no diffing at the node level — see `client.rs`'s module doc for
|
||||||
|
//! why the whole thing is simply rebuilt whenever a widget's spec changes.
|
||||||
|
//! This keeps the renderer a pure, stateless `WidgetNode -> gtk4::Widget`
|
||||||
|
//! function.
|
||||||
|
|
||||||
|
use super::client;
|
||||||
|
use bread_shared::widget::{
|
||||||
|
Align as StyleAlign, Background, FontWeight, Orientation as NodeOrientation, Padding, Radius,
|
||||||
|
SemanticColor, TextSize, WidgetNode, WidgetStyle,
|
||||||
|
};
|
||||||
|
use gtk4::prelude::*;
|
||||||
|
|
||||||
|
/// Default max width for a Label node, in characters, absent an explicit
|
||||||
|
/// `size`/other override — the `style` vocabulary (see Documentation.md's
|
||||||
|
/// Widgets §style) has no dedicated width field yet, so this stays fixed for
|
||||||
|
/// every label rather than becoming a half-exposed knob.
|
||||||
|
const DEFAULT_LABEL_MAX_WIDTH_CHARS: i32 = 32;
|
||||||
|
|
||||||
|
/// Curated bundled icons a widget can reference by name, so module authors
|
||||||
|
/// don't need to ship an SVG just to show a battery or bluetooth glyph.
|
||||||
|
/// Anything else goes through `icon.path` instead (see `bundled_or_path_icon`).
|
||||||
|
fn bundled_icon(name: &str) -> Option<&'static str> {
|
||||||
|
use crate::bar::stats::{
|
||||||
|
AC_POWER, BAT_HIGH, BAT_LOW, BAT_MID, BT_OFF, BT_ON, ICON_BRIGHTNESS, ICON_LOCK,
|
||||||
|
ICON_VOLUME, WIFI_OFF, WIFI_STRONG,
|
||||||
|
};
|
||||||
|
Some(match name {
|
||||||
|
"ac-power" => AC_POWER,
|
||||||
|
"battery-high" => BAT_HIGH,
|
||||||
|
"battery-mid" => BAT_MID,
|
||||||
|
"battery-low" => BAT_LOW,
|
||||||
|
"bluetooth-on" => BT_ON,
|
||||||
|
"bluetooth-off" => BT_OFF,
|
||||||
|
"wifi-strong" => WIFI_STRONG,
|
||||||
|
"wifi-off" => WIFI_OFF,
|
||||||
|
"lock" => ICON_LOCK,
|
||||||
|
"volume" => ICON_VOLUME,
|
||||||
|
"brightness" => ICON_BRIGHTNESS,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn icon_texture(
|
||||||
|
widget_id: &str,
|
||||||
|
name: Option<&str>,
|
||||||
|
path: Option<&str>,
|
||||||
|
px: u32,
|
||||||
|
) -> Option<gtk4::gdk::Texture> {
|
||||||
|
if let Some(n) = name {
|
||||||
|
return match bundled_icon(n) {
|
||||||
|
Some(svg) => Some(crate::svg_texture_sized(svg, px)),
|
||||||
|
None => {
|
||||||
|
eprintln!("breadbar: widget {widget_id}: unknown bundled icon name '{n}'");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let Some(path) = path else {
|
||||||
|
eprintln!("breadbar: widget {widget_id}: icon node has neither 'name' nor 'path'");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let expanded = bread_shared::expand_path(path);
|
||||||
|
match std::fs::read_to_string(&expanded) {
|
||||||
|
Ok(svg) => Some(crate::svg_texture_sized(&svg, px)),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("breadbar: widget {widget_id}: failed to read icon path '{path}': {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a node's typed `style` onto predefined CSS classes (see `theme.rs` for
|
||||||
|
/// the class definitions) — this is the only path from Lua's `style` field to
|
||||||
|
/// the widget, kept as narrow `Some(field) -> one class` mappings so there is
|
||||||
|
/// no way for it to become raw style injection.
|
||||||
|
fn apply_style(widget: >k4::Widget, style: &WidgetStyle) {
|
||||||
|
if let Some(color) = style.color {
|
||||||
|
widget.add_css_class(match color {
|
||||||
|
SemanticColor::Fg => "bread-color-fg",
|
||||||
|
SemanticColor::Dim => "bread-color-dim",
|
||||||
|
SemanticColor::Accent => "bread-color-accent",
|
||||||
|
SemanticColor::Red => "bread-color-red",
|
||||||
|
SemanticColor::Green => "bread-color-green",
|
||||||
|
SemanticColor::Yellow => "bread-color-yellow",
|
||||||
|
SemanticColor::Blue => "bread-color-blue",
|
||||||
|
SemanticColor::Pink => "bread-color-pink",
|
||||||
|
SemanticColor::Teal => "bread-color-teal",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(weight) = style.weight {
|
||||||
|
widget.add_css_class(match weight {
|
||||||
|
FontWeight::Normal => "bread-weight-normal",
|
||||||
|
FontWeight::Bold => "bread-weight-bold",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(size) = style.size {
|
||||||
|
widget.add_css_class(match size {
|
||||||
|
TextSize::Xs => "bread-size-xs",
|
||||||
|
TextSize::Sm => "bread-size-sm",
|
||||||
|
TextSize::Md => "bread-size-md",
|
||||||
|
TextSize::Lg => "bread-size-lg",
|
||||||
|
TextSize::Xl => "bread-size-xl",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(background) = style.background {
|
||||||
|
widget.add_css_class(match background {
|
||||||
|
Background::None => "bread-bg-none",
|
||||||
|
Background::Surface => "bread-bg-surface",
|
||||||
|
Background::Card => "bread-bg-card",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(radius) = style.radius {
|
||||||
|
widget.add_css_class(match radius {
|
||||||
|
Radius::None => "bread-radius-none",
|
||||||
|
Radius::Sm => "bread-radius-sm",
|
||||||
|
Radius::Md => "bread-radius-md",
|
||||||
|
Radius::Full => "bread-radius-full",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(padding) = style.padding {
|
||||||
|
widget.add_css_class(match padding {
|
||||||
|
Padding::None => "bread-padding-none",
|
||||||
|
Padding::Xs => "bread-padding-xs",
|
||||||
|
Padding::Sm => "bread-padding-sm",
|
||||||
|
Padding::Md => "bread-padding-md",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// GTK CSS has no text-align/justify-content equivalent — alignment is a
|
||||||
|
// widget property, not a stylesheet rule, so it's set directly instead
|
||||||
|
// of routing through an inert CSS class like the fields above.
|
||||||
|
if let Some(align) = style.align {
|
||||||
|
widget.set_halign(match align {
|
||||||
|
StyleAlign::Start => gtk4::Align::Start,
|
||||||
|
StyleAlign::Center => gtk4::Align::Center,
|
||||||
|
StyleAlign::End => gtk4::Align::End,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build (or rebuild) the GTK widget tree for `node`, belonging to widget
|
||||||
|
/// `widget_id` (fully-qualified `<module>.<id>`, used to tag any click).
|
||||||
|
pub fn build_node(node: &WidgetNode, widget_id: &str) -> gtk4::Widget {
|
||||||
|
let widget: gtk4::Widget = match node {
|
||||||
|
WidgetNode::Box {
|
||||||
|
orientation,
|
||||||
|
spacing,
|
||||||
|
children,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let gtk_orientation = match orientation {
|
||||||
|
NodeOrientation::Horizontal => gtk4::Orientation::Horizontal,
|
||||||
|
NodeOrientation::Vertical => gtk4::Orientation::Vertical,
|
||||||
|
};
|
||||||
|
let container = gtk4::Box::new(gtk_orientation, spacing.unwrap_or(4));
|
||||||
|
for child in children {
|
||||||
|
container.append(&build_node(child, widget_id));
|
||||||
|
}
|
||||||
|
container.upcast()
|
||||||
|
}
|
||||||
|
WidgetNode::Label { text, .. } => {
|
||||||
|
let label = gtk4::Label::new(Some(text));
|
||||||
|
// Unbounded, this is a bar-width-blowout waiting to happen from
|
||||||
|
// any buggy or malicious module — see Documentation.md issue #6.
|
||||||
|
label.set_ellipsize(gtk4::pango::EllipsizeMode::End);
|
||||||
|
label.set_max_width_chars(DEFAULT_LABEL_MAX_WIDTH_CHARS);
|
||||||
|
label.upcast()
|
||||||
|
}
|
||||||
|
WidgetNode::Icon { name, path, size, .. } => {
|
||||||
|
let px = size.unwrap_or(16).max(1) as u32;
|
||||||
|
let texture = icon_texture(widget_id, name.as_deref(), path.as_deref(), px);
|
||||||
|
let image = gtk4::Image::from_paintable(texture.as_ref());
|
||||||
|
image.upcast()
|
||||||
|
}
|
||||||
|
WidgetNode::Progress { value, .. } => {
|
||||||
|
let bar = gtk4::ProgressBar::new();
|
||||||
|
bar.set_fraction(value.clamp(0.0, 1.0));
|
||||||
|
// GtkProgressBar's natural expand behavior is to fill all
|
||||||
|
// available width, which — unlike Label/Box/Image, which hug
|
||||||
|
// their content by default — propagates up through every
|
||||||
|
// ancestor Box that doesn't set hexpand explicitly, all the way
|
||||||
|
// to the bar's end_widget. Pin it to a small fixed footprint so
|
||||||
|
// it reads as an inline meter instead of swallowing the bar.
|
||||||
|
bar.set_hexpand(false);
|
||||||
|
bar.set_valign(gtk4::Align::Center);
|
||||||
|
bar.set_size_request(40, 6);
|
||||||
|
bar.upcast()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
widget.add_css_class("bread-widget-node");
|
||||||
|
if let Some(class) = node.class() {
|
||||||
|
widget.add_css_class(class);
|
||||||
|
}
|
||||||
|
if let Some(style) = node.style() {
|
||||||
|
apply_style(&widget, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(action) = node.on_click() {
|
||||||
|
widget.add_css_class("clickable");
|
||||||
|
let widget_id = widget_id.to_string();
|
||||||
|
let action = action.clone();
|
||||||
|
let gesture = gtk4::GestureClick::new();
|
||||||
|
gesture.connect_released(move |_, _, _, _| {
|
||||||
|
client::emit_click(&widget_id, &action);
|
||||||
|
});
|
||||||
|
widget.add_controller(gesture);
|
||||||
|
}
|
||||||
|
|
||||||
|
widget
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue