breadman: rebuild settings with libadwaita components

Proof-of-concept for the ecosystem-wide design-system work: every setting
is now an AdwPreferencesGroup/SwitchRow/SpinRow/ActionRow/ComboRow via
bread-theme::adw, instead of a hand-rolled GtkGrid of labels + raw widgets.

This directly fixes the two worst defects a design critique found here:
- Toggle switches rendered ~1400px wide, like progress bars — caused by
  GtkGrid sizing a switch's column to match a sibling entry's hexpand in
  the same grid, and Switch has no default that resists filling that
  width. AdwSwitchRow owns its own layout, so this class of bug can't
  recur.
- Spin buttons were ~1300px from the label they modified, for the same
  grid-column-sizing reason. AdwSpinRow docks the spinner to its title.

Also softened the jargon-forward section titles the critique flagged
("Model (Tier 2 ONNX)", "Ollama (Tier 3)") — the tier/implementation
detail moved from the title (loud, unavoidable) to each group's
description (quieter, still there for anyone who wants it).

bread-theme now needs a direct dependency here (branch=dev, `adw`
feature) since breadpad-shared's own bread-theme dependency is tag-pinned
to a release before this feature existed.
This commit is contained in:
Breadway 2026-07-30 18:39:56 +08:00
parent b8a37cbb85
commit b85d682721
4 changed files with 189 additions and 143 deletions

70
Cargo.lock generated
View file

@ -76,7 +76,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@ -87,7 +87,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"once_cell_polyfill", "once_cell_polyfill",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@ -305,7 +305,7 @@ dependencies = [
[[package]] [[package]]
name = "bread-screenshots" name = "bread-screenshots"
version = "0.3.1" version = "0.3.1"
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#1a3475bd2358202f60e29c9bd27d06b1428b1a27" source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#4e76bc707720fbfc52e19a8cc78f3fb1e6e0a713"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bread-utils", "bread-utils",
@ -323,10 +323,22 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "bread-theme"
version = "0.3.1"
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#4e76bc707720fbfc52e19a8cc78f3fb1e6e0a713"
dependencies = [
"dirs 5.0.1",
"gtk4",
"libadwaita",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "bread-utils" name = "bread-utils"
version = "0.3.1" version = "0.3.1"
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#1a3475bd2358202f60e29c9bd27d06b1428b1a27" source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#4e76bc707720fbfc52e19a8cc78f3fb1e6e0a713"
dependencies = [ dependencies = [
"dirs 5.0.1", "dirs 5.0.1",
"serde", "serde",
@ -339,11 +351,13 @@ version = "0.3.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bread-screenshots", "bread-screenshots",
"bread-theme 0.3.1",
"breadpad-shared", "breadpad-shared",
"chrono", "chrono",
"dirs 5.0.1", "dirs 5.0.1",
"futures-channel", "futures-channel",
"gtk4", "gtk4",
"libadwaita",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
@ -376,7 +390,7 @@ name = "breadpad-shared"
version = "0.3.4" version = "0.3.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bread-theme", "bread-theme 0.2.3",
"chrono", "chrono",
"dirs 5.0.1", "dirs 5.0.1",
"ical", "ical",
@ -873,7 +887,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users 0.5.2", "redox_users 0.5.2",
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -948,7 +962,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -1259,7 +1273,7 @@ dependencies = [
"gobject-sys", "gobject-sys",
"libc", "libc",
"system-deps", "system-deps",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -1879,6 +1893,36 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libadwaita"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b9900e67182a4b5b1f157b448d94f0715c8b9770cce21cf000801917f53bfa"
dependencies = [
"gdk4",
"gio",
"glib",
"gtk4",
"libadwaita-sys",
"pango",
]
[[package]]
name = "libadwaita-sys"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3c27642b389852aa99341bd4a4c19ec6f8a2b63ebdd7f5ba1952198079ccd"
dependencies = [
"gdk4-sys",
"gio-sys",
"glib-sys",
"gobject-sys",
"gtk4-sys",
"libc",
"pango-sys",
"system-deps",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.186" version = "0.2.186"
@ -2101,7 +2145,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -2723,7 +2767,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -2937,7 +2981,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@ -3047,7 +3091,7 @@ dependencies = [
"getrandom 0.4.2", "getrandom 0.4.2",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -3415,7 +3459,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [ dependencies = [
"memoffset", "memoffset",
"tempfile", "tempfile",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]

View file

@ -13,6 +13,11 @@ path = "src/main.rs"
breadpad-shared = { path = "../breadpad-shared" } breadpad-shared = { path = "../breadpad-shared" }
# Capture primitives for `--screenshot` mode — see src/screenshot.rs. # Capture primitives for `--screenshot` mode — see src/screenshot.rs.
bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "dev" } bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "dev" }
# libadwaita-based settings-screen components (see src/views/settings.rs) —
# on "dev", not breadpad-shared's tag-pinned copy, since the `adw` feature
# doesn't exist on a tagged release yet.
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "dev", features = ["adw"] }
libadwaita = { version = "0.9", features = ["v1_7"] }
anyhow.workspace = true anyhow.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber.workspace = true tracing-subscriber.workspace = true

View file

@ -342,6 +342,10 @@ fn build_app_window(
screenshot_req: Option<screenshot::ScreenshotRequest>, screenshot_req: Option<screenshot::ScreenshotRequest>,
) -> Result<()> { ) -> Result<()> {
apply_css(&cfg); apply_css(&cfg);
// Needed once before constructing any adw:: widget (see views::settings) —
// also forces dark mode, since bread-theme's palette is a fixed dark base
// regardless of the system GTK preference.
bread_theme::adw::init();
let store = Arc::new(Store::new()?); let store = Arc::new(Store::new()?);
let notes = store.load_all()?; let notes = store.load_all()?;

View file

@ -1,7 +1,9 @@
use breadpad_shared::config::{ use breadpad_shared::config::{
CalendarConfig, Config, ModelConfig, OllamaConfig, RemindersConfig, Settings, CalendarConfig, Config, ModelConfig, OllamaConfig, RemindersConfig, Settings,
}; };
use bread_theme::adw;
use gtk4::prelude::*; use gtk4::prelude::*;
use libadwaita::prelude::*;
pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::ScrolledWindow { pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::ScrolledWindow {
let scroll = gtk4::ScrolledWindow::builder() let scroll = gtk4::ScrolledWindow::builder()
@ -10,139 +12,148 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled
.vexpand(true) .vexpand(true)
.build(); .build();
let outer = gtk4::Box::builder() let page = libadwaita::PreferencesPage::new();
.orientation(gtk4::Orientation::Vertical)
.spacing(16)
.margin_top(16)
.margin_bottom(16)
.margin_start(16)
.margin_end(16)
.build();
// ── General ────────────────────────────────────────────────── // ── General ──────────────────────────────────────────────────
let (general_frame, general_grid) = make_section("General"); let general_group = adw::preferences_group("General", None);
let type_options = ["note", "todo", "reminder", "idea", "question"]; let type_options = ["note", "todo", "reminder", "idea", "question"];
let default_type_combo = gtk4::DropDown::from_strings(&type_options); let default_type_row = libadwaita::ComboRow::builder()
.title("Default type")
.model(&gtk4::StringList::new(&type_options))
.build();
let dt_idx = type_options let dt_idx = type_options
.iter() .iter()
.position(|&s| s == cfg.settings.default_type.as_str()) .position(|&s| s == cfg.settings.default_type.as_str())
.unwrap_or(0) as u32; .unwrap_or(0) as u32;
default_type_combo.set_selected(dt_idx); default_type_row.set_selected(dt_idx);
attach_row(&general_grid, 0, "Default type", &default_type_combo); general_group.add(&default_type_row);
let ws_tag_switch = gtk4::Switch::builder() let ws_tag_row = adw::toggle_row(
.active(cfg.settings.workspace_tag) "Workspace tag",
Some("Tag new notes with the Hyprland workspace they were created on"),
cfg.settings.workspace_tag,
);
general_group.add(&ws_tag_row);
let archive_adj = gtk4::Adjustment::new(cfg.settings.archive_after_days as f64, 1.0, 365.0, 1.0, 7.0, 0.0);
let archive_row = adw::spin_row("Archive after (days)", None, &archive_adj);
general_group.add(&archive_row);
let snooze_row = adw::action_row("Snooze options", Some("Comma-separated (e.g. 15m, 1h, tomorrow_morning)"));
let snooze_entry = gtk4::Entry::builder()
.text(cfg.settings.snooze_options.join(", "))
.valign(gtk4::Align::Center) .valign(gtk4::Align::Center)
.build(); .build();
attach_row(&general_grid, 1, "Workspace tag", &ws_tag_switch); snooze_row.add_suffix(&snooze_entry);
general_group.add(&snooze_row);
let archive_spin = gtk4::SpinButton::with_range(1.0, 365.0, 1.0); page.add(&general_group);
archive_spin.set_value(cfg.settings.archive_after_days as f64);
attach_row(&general_grid, 2, "Archive after (days)", &archive_spin);
let snooze_entry = gtk4::Entry::builder()
.text(&cfg.settings.snooze_options.join(", "))
.hexpand(true)
.build();
attach_row(&general_grid, 3, "Snooze options", &snooze_entry);
outer.append(&general_frame);
// ── Reminders ──────────────────────────────────────────────── // ── Reminders ────────────────────────────────────────────────
let (rem_frame, rem_grid) = make_section("Reminders"); let rem_group = adw::preferences_group("Reminders", None);
let morning_row = adw::action_row("Default morning", Some("Used for \"tomorrow_morning\" snoozes and recurring reminders"));
let morning_entry = gtk4::Entry::builder() let morning_entry = gtk4::Entry::builder()
.text(&cfg.reminders.default_morning) .text(&cfg.reminders.default_morning)
.placeholder_text("HH:MM") .placeholder_text("HH:MM")
.build();
attach_row(&rem_grid, 0, "Default morning", &morning_entry);
let grace_spin = gtk4::SpinButton::with_range(0.0, 1440.0, 5.0);
grace_spin.set_value(cfg.reminders.missed_grace_minutes as f64);
attach_row(&rem_grid, 1, "Missed grace (minutes)", &grace_spin);
outer.append(&rem_frame);
// ── Model ─────────────────────────────────────────────────────
let (model_frame, model_grid) = make_section("Model (Tier 2 ONNX)");
let model_path_entry = gtk4::Entry::builder()
.text(&cfg.model.path)
.hexpand(true)
.build();
attach_row(&model_grid, 0, "ONNX path", &model_path_entry);
let tokenizer_entry = gtk4::Entry::builder()
.text(&cfg.model.tokenizer)
.hexpand(true)
.build();
attach_row(&model_grid, 1, "Tokenizer path", &tokenizer_entry);
let ort_dylib_entry = gtk4::Entry::builder()
.text(&cfg.model.ort_dylib_path)
.hexpand(true)
.build();
attach_row(&model_grid, 2, "ORT dylib path", &ort_dylib_entry);
outer.append(&model_frame);
// ── Ollama (Tier 3) ───────────────────────────────────────────
let (ollama_frame, ollama_grid) = make_section("Ollama (Tier 3)");
let ollama_enabled = gtk4::Switch::builder()
.active(cfg.model.ollama.enabled)
.valign(gtk4::Align::Center) .valign(gtk4::Align::Center)
.build(); .build();
attach_row(&ollama_grid, 0, "Enabled", &ollama_enabled); morning_row.add_suffix(&morning_entry);
rem_group.add(&morning_row);
let ollama_endpoint = gtk4::Entry::builder() let grace_adj = gtk4::Adjustment::new(cfg.reminders.missed_grace_minutes as f64, 0.0, 1440.0, 5.0, 30.0, 0.0);
.text(&cfg.model.ollama.endpoint) let grace_row = adw::spin_row("Missed grace (minutes)", Some("How late a reminder can fire before it's considered missed"), &grace_adj);
.hexpand(true) rem_group.add(&grace_row);
.build();
attach_row(&ollama_grid, 1, "Endpoint", &ollama_endpoint);
let ollama_model = gtk4::Entry::builder() page.add(&rem_group);
.text(&cfg.model.ollama.model)
.build();
attach_row(&ollama_grid, 2, "Model", &ollama_model);
let ollama_thresh = gtk4::SpinButton::with_range(0.0, 1.0, 0.05); // ── Local classifier ───────────────────────────────────────────
ollama_thresh.set_value(cfg.model.ollama.confidence_threshold as f64); let model_group = adw::preferences_group(
ollama_thresh.set_digits(2); "Local Classifier",
attach_row(&ollama_grid, 3, "Confidence threshold", &ollama_thresh); Some("Optional local ONNX model for classifying note type/time without a network round-trip."),
);
outer.append(&ollama_frame); let model_path_row = adw::action_row("Model path", None);
let model_path_entry = gtk4::Entry::builder().text(&cfg.model.path).hexpand(true).width_chars(36).valign(gtk4::Align::Center).build();
model_path_row.add_suffix(&model_path_entry);
model_group.add(&model_path_row);
// ── Calendar ───────────────────────────────────────────────── let tokenizer_row = adw::action_row("Tokenizer path", None);
let (cal_frame, cal_grid) = make_section("Nextcloud Calendar (CalDAV)"); let tokenizer_entry = gtk4::Entry::builder().text(&cfg.model.tokenizer).hexpand(true).width_chars(36).valign(gtk4::Align::Center).build();
tokenizer_row.add_suffix(&tokenizer_entry);
model_group.add(&tokenizer_row);
let cal_enabled = gtk4::Switch::builder() let ort_dylib_row = adw::action_row("Runtime library path", None);
.active(cfg.calendar.enabled) let ort_dylib_entry = gtk4::Entry::builder().text(&cfg.model.ort_dylib_path).hexpand(true).width_chars(36).valign(gtk4::Align::Center).build();
.valign(gtk4::Align::Center) ort_dylib_row.add_suffix(&ort_dylib_entry);
.build(); model_group.add(&ort_dylib_row);
attach_row(&cal_grid, 0, "Enabled", &cal_enabled);
page.add(&model_group);
// ── AI classification (Ollama) ──────────────────────────────────
let ollama_group = adw::preferences_group(
"AI Classification",
Some("Uses a local Ollama model as a fallback classifier when the ONNX model is unavailable or unsure."),
);
let ollama_enabled_row = adw::toggle_row("Enabled", None, cfg.model.ollama.enabled);
ollama_group.add(&ollama_enabled_row);
let ollama_endpoint_row = adw::action_row("Endpoint", None);
let ollama_endpoint_entry = gtk4::Entry::builder().text(&cfg.model.ollama.endpoint).hexpand(true).width_chars(36).valign(gtk4::Align::Center).build();
ollama_endpoint_row.add_suffix(&ollama_endpoint_entry);
ollama_group.add(&ollama_endpoint_row);
let ollama_model_row = adw::action_row("Model", None);
let ollama_model_entry = gtk4::Entry::builder().text(&cfg.model.ollama.model).valign(gtk4::Align::Center).build();
ollama_model_row.add_suffix(&ollama_model_entry);
ollama_group.add(&ollama_model_row);
let ollama_thresh_adj = gtk4::Adjustment::new(cfg.model.ollama.confidence_threshold as f64, 0.0, 1.0, 0.05, 0.1, 0.0);
let ollama_thresh_row = adw::spin_row("Confidence threshold", None, &ollama_thresh_adj);
if let Some(spin) = ollama_thresh_row.first_child().and_downcast::<gtk4::SpinButton>() {
spin.set_digits(2);
}
ollama_group.add(&ollama_thresh_row);
page.add(&ollama_group);
// ── Calendar sync ────────────────────────────────────────────
let cal_group = adw::preferences_group(
"Calendar Sync",
Some("Sync reminders to a Nextcloud calendar via CalDAV."),
);
let cal_enabled_row = adw::toggle_row("Enabled", None, cfg.calendar.enabled);
cal_group.add(&cal_enabled_row);
let cal_url_row = adw::action_row("Calendar URL", None);
let cal_url = gtk4::Entry::builder() let cal_url = gtk4::Entry::builder()
.text(&cfg.calendar.url) .text(&cfg.calendar.url)
.placeholder_text("https://nextcloud.example.com/remote.php/dav/calendars/you/personal/") .placeholder_text("https://nextcloud.example.com/remote.php/dav/calendars/you/personal/")
.hexpand(true) .hexpand(true)
.width_chars(36)
.valign(gtk4::Align::Center)
.build(); .build();
attach_row(&cal_grid, 1, "Calendar URL", &cal_url); cal_url_row.add_suffix(&cal_url);
cal_group.add(&cal_url_row);
let cal_user = gtk4::Entry::builder() let cal_user_row = adw::action_row("Username", None);
.text(&cfg.calendar.username) let cal_user = gtk4::Entry::builder().text(&cfg.calendar.username).valign(gtk4::Align::Center).build();
.build(); cal_user_row.add_suffix(&cal_user);
attach_row(&cal_grid, 2, "Username", &cal_user); cal_group.add(&cal_user_row);
let cal_pass = gtk4::Entry::builder() let cal_pass_row = adw::action_row("App password", None);
let cal_pass = gtk4::PasswordEntry::builder()
.text(&cfg.calendar.password) .text(&cfg.calendar.password)
.input_purpose(gtk4::InputPurpose::Password) .show_peek_icon(true)
.visibility(false) .valign(gtk4::Align::Center)
.build(); .build();
attach_row(&cal_grid, 3, "App password", &cal_pass); cal_pass_row.add_suffix(&cal_pass);
cal_group.add(&cal_pass_row);
outer.append(&cal_frame); page.add(&cal_group);
// ── Save ────────────────────────────────────────────────────── // ── Save ──────────────────────────────────────────────────────
let status_label = gtk4::Label::builder() let status_label = gtk4::Label::builder()
@ -157,20 +168,20 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled
.build(); .build();
{ {
let dtc = default_type_combo.clone(); let dtc = default_type_row.clone();
let wts = ws_tag_switch.clone(); let wts = ws_tag_row.clone();
let ars = archive_spin.clone(); let ars = archive_adj.clone();
let sne = snooze_entry.clone(); let sne = snooze_entry.clone();
let moe = morning_entry.clone(); let moe = morning_entry.clone();
let grs = grace_spin.clone(); let grs = grace_adj.clone();
let mpe = model_path_entry.clone(); let mpe = model_path_entry.clone();
let tke = tokenizer_entry.clone(); let tke = tokenizer_entry.clone();
let ode = ort_dylib_entry.clone(); let ode = ort_dylib_entry.clone();
let oec = ollama_enabled.clone(); let oec = ollama_enabled_row.clone();
let oee = ollama_endpoint.clone(); let oee = ollama_endpoint_entry.clone();
let ome = ollama_model.clone(); let ome = ollama_model_entry.clone();
let ots = ollama_thresh.clone(); let ots = ollama_thresh_adj.clone();
let cec = cal_enabled.clone(); let cec = cal_enabled_row.clone();
let cuc = cal_url.clone(); let cuc = cal_url.clone();
let csc = cal_user.clone(); let csc = cal_user.clone();
let cpc = cal_pass.clone(); let cpc = cal_pass.clone();
@ -228,37 +239,19 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled
let btn_row = gtk4::Box::builder() let btn_row = gtk4::Box::builder()
.orientation(gtk4::Orientation::Horizontal) .orientation(gtk4::Orientation::Horizontal)
.spacing(8) .spacing(8)
.margin_top(16)
.margin_start(16)
.margin_end(16)
.margin_bottom(16)
.build(); .build();
btn_row.append(&status_label); btn_row.append(&status_label);
btn_row.append(&gtk4::Box::builder().hexpand(true).build()); btn_row.append(&gtk4::Box::builder().hexpand(true).build());
btn_row.append(&save_btn); btn_row.append(&save_btn);
let outer = gtk4::Box::builder().orientation(gtk4::Orientation::Vertical).build();
outer.append(&page);
outer.append(&btn_row); outer.append(&btn_row);
scroll.set_child(Some(&outer)); scroll.set_child(Some(&outer));
scroll scroll
} }
fn make_section(title: &str) -> (gtk4::Frame, gtk4::Grid) {
let frame = gtk4::Frame::builder().label(title).build();
let grid = gtk4::Grid::builder()
.row_spacing(8)
.column_spacing(16)
.margin_top(8)
.margin_bottom(8)
.margin_start(8)
.margin_end(8)
.build();
frame.set_child(Some(&grid));
(frame, grid)
}
fn attach_row(grid: &gtk4::Grid, row: i32, label: &str, widget: &impl gtk4::prelude::IsA<gtk4::Widget>) {
let lbl = gtk4::Label::builder()
.label(label)
.xalign(0.0)
.hexpand(false)
.width_chars(24)
.build();
grid.attach(&lbl, 0, row, 1, 1);
grid.attach(widget, 1, row, 1, 1);
}