diff --git a/Cargo.lock b/Cargo.lock index 1152693..17001a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,21 +312,9 @@ dependencies = [ "tracing", ] -[[package]] -name = "bread-theme" -version = "0.2.3" -source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.8#77417d552130281ff787e07d52541eb25e9d533b" -dependencies = [ - "dirs 5.0.1", - "gtk4", - "serde", - "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", @@ -351,7 +339,7 @@ version = "0.3.4" dependencies = [ "anyhow", "bread-screenshots", - "bread-theme 0.3.1", + "bread-theme", "breadpad-shared", "chrono", "dirs 5.0.1", @@ -390,7 +378,7 @@ name = "breadpad-shared" version = "0.3.4" dependencies = [ "anyhow", - "bread-theme 0.2.3", + "bread-theme", "chrono", "dirs 5.0.1", "ical", @@ -598,7 +586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -887,7 +875,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -962,7 +950,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1273,7 +1261,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2145,7 +2133,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2767,7 +2755,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3091,7 +3079,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/breadman/src/editor.rs b/breadman/src/editor.rs index bebf15d..a11e46e 100644 --- a/breadman/src/editor.rs +++ b/breadman/src/editor.rs @@ -23,8 +23,11 @@ use std::sync::Arc; pub const TIME_PLACEHOLDER: &str = "tomorrow 9am / at 7pm / 2026-08-01 09:00"; pub const RRULE_PLACEHOLDER: &str = "RRULE:FREQ=WEEKLY;BYDAY=MO"; +/// Builds the dialog but does not present it - callers that need to hook +/// its `map` signal (screenshot mode) must connect before presenting or +/// they miss the signal entirely; interactive callers present immediately +/// with `dialog.present(Some(parent))`. pub fn open_editor( - parent: >k4::Widget, note: &Note, store: Arc, morning: String, @@ -35,6 +38,7 @@ pub fn open_editor( let dialog = libadwaita::Dialog::builder() .title("Edit Note") .content_width(480) + .content_height(520) .build(); let header = libadwaita::HeaderBar::new(); @@ -114,6 +118,8 @@ pub fn open_editor( let scroll = gtk4::ScrolledWindow::builder() .hscrollbar_policy(gtk4::PolicyType::Never) .vscrollbar_policy(gtk4::PolicyType::Automatic) + .vexpand(true) + .min_content_height(400) .build(); scroll.set_child(Some(&content)); toolbar_view.set_content(Some(&scroll)); @@ -200,7 +206,6 @@ pub fn open_editor( }); } - dialog.present(Some(parent)); dialog } diff --git a/breadman/src/screenshot.rs b/breadman/src/screenshot.rs index d579ad1..5b36c9d 100644 --- a/breadman/src/screenshot.rs +++ b/breadman/src/screenshot.rs @@ -20,6 +20,7 @@ //! callbacks since nothing here should actually persist a change. use gtk4::prelude::*; +use libadwaita::prelude::*; use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; @@ -92,9 +93,10 @@ pub fn dispatch( let morning = state.cfg.borrow().reminders.default_morning.clone(); let store = Arc::new(state.write_store()); // AdwDialog handles its own presentation/centering - no more - // manual popover anchor/position/autohide juggling. + // manual popover anchor/position/autohide juggling. Must + // connect `map` BEFORE presenting, or the signal (which can + // fire synchronously inside `present`) is missed entirely. let dialog = crate::editor::open_editor( - root.upcast_ref::(), ¬e, store, morning, @@ -109,6 +111,7 @@ pub fn dispatch( finish(bread_screenshots::capture_region(0, 0, width, height, &output)); }); }); + dialog.present(Some(root.upcast_ref::())); }); }); return; diff --git a/breadman/src/views/row.rs b/breadman/src/views/row.rs index 4e07994..501b8a0 100644 --- a/breadman/src/views/row.rs +++ b/breadman/src/views/row.rs @@ -8,6 +8,7 @@ use breadpad_shared::types::{Note, NoteType}; use gtk4::prelude::*; +use libadwaita::prelude::*; use std::rc::Rc; pub struct RowSpec<'a> { @@ -140,8 +141,7 @@ pub fn build(spec: RowSpec, state: crate::AppState) -> gtk4::Box { let row_del = row_c.clone(); let state_err = state_c.clone(); - crate::editor::open_editor( - btn.upcast_ref::(), + let dialog = crate::editor::open_editor( ¬e_c, store, morning, @@ -163,6 +163,7 @@ pub fn build(spec: RowSpec, state: crate::AppState) -> gtk4::Box { state_err.log_error(e); }), ); + dialog.present(Some(btn.upcast_ref::())); }); } row.append(&edit_btn); diff --git a/breadman/src/views/settings.rs b/breadman/src/views/settings.rs index 52bacd0..6090274 100644 --- a/breadman/src/views/settings.rs +++ b/breadman/src/views/settings.rs @@ -1,13 +1,81 @@ +//! Settings screen. Deliberately plain GTK4, not libadwaita's AdwActionRow/ +//! AdwSpinRow/AdwEntryRow family — those ran noticeably taller than the rest +//! of the app and don't expose a way to constrain the internal spin +//! button's width from the outside (it stretches to fill whatever room the +//! row has, leaving the digits and +/- buttons stranded behind a huge empty +//! bordered box once the row is wider than libadwaita's usual ~400-600px +//! home turf). Instead this mirrors bos-settings' own Row.svelte / +//! NumberField.svelte / TextField.svelte design exactly — same tokens +//! (12/16px row padding, ch-width inputs, transparent-at-rest border) — so +//! the two settings screens in the ecosystem actually agree with each other. + use breadpad_shared::config::{ CalendarConfig, Config, ModelConfig, OllamaConfig, RemindersConfig, Settings, }; use breadpad_shared::types::NoteType; -use bread_theme::adw; -use gtk4::prelude::*; -use libadwaita::prelude::*; +use gtk4::{glib, prelude::*}; use std::cell::RefCell; use std::rc::Rc; +/// A titled group: heading, optional description, then a `.boxed-list` of +/// `field_row`s (native GTK4 rounded-corner-run + divider styling). +fn field_group(title: &str, description: Option<&str>) -> (gtk4::Box, gtk4::ListBox) { + let outer = gtk4::Box::builder().orientation(gtk4::Orientation::Vertical).spacing(8).build(); + + let heading = gtk4::Label::builder().label(title).xalign(0.0).css_classes(["heading"]).build(); + outer.append(&heading); + + if let Some(desc) = description { + let desc_label = gtk4::Label::builder() + .label(desc) + .xalign(0.0) + .wrap(true) + .css_classes(["dim-label"]) + .build(); + outer.append(&desc_label); + } + + let list = gtk4::ListBox::builder() + .selection_mode(gtk4::SelectionMode::None) + .css_classes(["boxed-list"]) + .build(); + outer.append(&list); + + (outer, list) +} + +/// A single row: label (+ optional subtitle) on the left, one control on +/// the right — same shape as bos-settings' `Row.svelte`. +fn field_row(label: &str, subtitle: Option<&str>, control: &impl IsA) -> gtk4::ListBoxRow { + let row = gtk4::ListBoxRow::builder() + .selectable(false) + .activatable(false) + .css_classes(["field-row"]) + .build(); + + let hbox = gtk4::Box::builder().orientation(gtk4::Orientation::Horizontal).spacing(16).build(); + + let label_box = gtk4::Box::builder().orientation(gtk4::Orientation::Vertical).hexpand(true).valign(gtk4::Align::Center).build(); + label_box.append(>k4::Label::builder().label(label).xalign(0.0).build()); + if let Some(sub) = subtitle { + label_box.append(>k4::Label::builder().label(sub).xalign(0.0).wrap(true).css_classes(["field-row-subtitle"]).build()); + } + hbox.append(&label_box); + hbox.append(control); + + row.set_child(Some(&hbox)); + row +} + +fn text_entry(text: &str, width_chars: i32) -> gtk4::Entry { + gtk4::Entry::builder().text(text).width_chars(width_chars).valign(gtk4::Align::Center).css_classes(["field-input"]).build() +} + +fn spin_button(value: f64, min: f64, max: f64, step: f64, page: f64, digits: u32) -> gtk4::SpinButton { + let adj = gtk4::Adjustment::new(value, min, max, step, page, 0.0); + gtk4::SpinButton::builder().adjustment(&adj).digits(digits).width_chars(8).valign(gtk4::Align::Center).css_classes(["field-input"]).build() +} + pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::ScrolledWindow { let scroll = gtk4::ScrolledWindow::builder() .hscrollbar_policy(gtk4::PolicyType::Never) @@ -15,25 +83,12 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled .vexpand(true) .build(); - // Plain vertical box (not AdwPreferencesPage) wrapped in our own Clamp — - // PreferencesPage's built-in clamp caps out around ~600px, far narrower - // than the rest of breadman's edge-to-edge views. 900px + left-aligned - // keeps forms readable without the settings screen reading as a - // different, narrower app bolted onto the side. - let content = gtk4::Box::builder() - .orientation(gtk4::Orientation::Vertical) - .spacing(24) - .build(); + let content = gtk4::Box::builder().orientation(gtk4::Orientation::Vertical).spacing(24).build(); // ── General ────────────────────────────────────────────────── - let general_group = adw::preferences_group("General", None); + let (general_group, general_list) = field_group("General", None); - let default_type_row = adw::action_row("Default type", None); - let type_pill_box = gtk4::Box::builder() - .orientation(gtk4::Orientation::Horizontal) - .spacing(4) - .valign(gtk4::Align::Center) - .build(); + let type_pill_box = gtk4::Box::builder().orientation(gtk4::Orientation::Horizontal).spacing(4).valign(gtk4::Align::Center).build(); let selected_type: Rc> = Rc::new(RefCell::new(cfg.settings.default_type.clone())); let type_pills: Vec<(gtk4::Button, &'static str)> = NoteType::all_builtin() .iter() @@ -43,181 +98,148 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled bread_theme::gtk::set_chip_active(btn, *name == selected_type.borrow().as_str()); type_pill_box.append(btn); } - default_type_row.add_suffix(&type_pill_box); - general_group.add(&default_type_row); + general_list.append(&field_row("Default type", None, &type_pill_box)); - let ws_tag_row = adw::toggle_row( + let ws_tag_switch = gtk4::Switch::builder().active(cfg.settings.workspace_tag).valign(gtk4::Align::Center).build(); + general_list.append(&field_row( "Workspace tag", Some("Tag new notes with the Hyprland workspace they were created on"), - cfg.settings.workspace_tag, - ); - general_group.add(&ws_tag_row); + &ws_tag_switch, + )); - 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 archive_spin = spin_button(cfg.settings.archive_after_days as f64, 1.0, 365.0, 1.0, 7.0, 0); + general_list.append(&field_row("Archive after (days)", None, &archive_spin)); - let snooze_row = libadwaita::EntryRow::builder() - .title("Snooze options") - .show_apply_button(true) - .build(); - snooze_row.set_text(&cfg.settings.snooze_options.join(", ")); - general_group.add(&snooze_row); + let snooze_entry = text_entry(&cfg.settings.snooze_options.join(", "), 24); + general_list.append(&field_row("Snooze options", Some("Comma-separated (e.g. 15m, 1h, tomorrow_morning)"), &snooze_entry)); content.append(&general_group); // ── Reminders ──────────────────────────────────────────────── - let rem_group = adw::preferences_group("Reminders", None); + let (rem_group, rem_list) = field_group("Reminders", None); - let morning_row = libadwaita::EntryRow::builder() - .title("Default morning (used for \"tomorrow_morning\" snoozes and recurring reminders)") - .show_apply_button(true) - .build(); - morning_row.set_text(&cfg.reminders.default_morning); - rem_group.add(&morning_row); + let morning_entry = text_entry(&cfg.reminders.default_morning, 10); + rem_list.append(&field_row("Default morning", Some("Used for \"tomorrow_morning\" snoozes and recurring reminders"), &morning_entry)); - let grace_adj = gtk4::Adjustment::new(cfg.reminders.missed_grace_minutes as f64, 0.0, 1440.0, 5.0, 30.0, 0.0); - let grace_row = adw::spin_row("Missed grace (minutes)", Some("How late a reminder can fire before it's considered missed"), &grace_adj); - rem_group.add(&grace_row); + let grace_spin = spin_button(cfg.reminders.missed_grace_minutes as f64, 0.0, 1440.0, 5.0, 30.0, 0); + rem_list.append(&field_row("Missed grace (minutes)", Some("How late a reminder can fire before it's considered missed"), &grace_spin)); content.append(&rem_group); // ── Local classifier ─────────────────────────────────────────── - let model_group = adw::preferences_group( + let (model_group, model_list) = field_group( "Local Classifier", Some("Optional local ONNX model for classifying note type/time without a network round-trip. These paths are shared with breadpad — both apps read the same model files."), ); - let model_path_row = libadwaita::EntryRow::builder().title("Model path").show_apply_button(true).build(); - model_path_row.set_text(&cfg.model.path); - model_group.add(&model_path_row); + let model_path_entry = text_entry(&cfg.model.path, 30); + model_list.append(&field_row("Model path", None, &model_path_entry)); - let tokenizer_row = libadwaita::EntryRow::builder().title("Tokenizer path").show_apply_button(true).build(); - tokenizer_row.set_text(&cfg.model.tokenizer); - model_group.add(&tokenizer_row); + let tokenizer_entry = text_entry(&cfg.model.tokenizer, 30); + model_list.append(&field_row("Tokenizer path", None, &tokenizer_entry)); - let ort_dylib_row = libadwaita::EntryRow::builder().title("Runtime library path").show_apply_button(true).build(); - ort_dylib_row.set_text(&cfg.model.ort_dylib_path); - model_group.add(&ort_dylib_row); + let ort_dylib_entry = text_entry(&cfg.model.ort_dylib_path, 30); + model_list.append(&field_row("Runtime library path", None, &ort_dylib_entry)); content.append(&model_group); // ── AI classification (Ollama) ────────────────────────────────── - let ollama_group = adw::preferences_group( + let (ollama_group, ollama_list) = field_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_enabled_switch = gtk4::Switch::builder().active(cfg.model.ollama.enabled).valign(gtk4::Align::Center).build(); + ollama_list.append(&field_row("Enabled", None, &ollama_enabled_switch)); - let ollama_endpoint_row = libadwaita::EntryRow::builder().title("Endpoint").show_apply_button(true).build(); - ollama_endpoint_row.set_text(&cfg.model.ollama.endpoint); - ollama_group.add(&ollama_endpoint_row); + let ollama_endpoint_entry = text_entry(&cfg.model.ollama.endpoint, 24); + ollama_list.append(&field_row("Endpoint", None, &ollama_endpoint_entry)); - let ollama_model_row = libadwaita::EntryRow::builder().title("Model").show_apply_button(true).build(); - ollama_model_row.set_text(&cfg.model.ollama.model); - ollama_group.add(&ollama_model_row); + let ollama_model_entry = text_entry(&cfg.model.ollama.model, 16); + ollama_list.append(&field_row("Model", None, &ollama_model_entry)); - 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::() { - spin.set_digits(2); - } - ollama_group.add(&ollama_thresh_row); + let ollama_thresh_spin = spin_button(cfg.model.ollama.confidence_threshold as f64, 0.0, 1.0, 0.05, 0.1, 2); + ollama_list.append(&field_row("Confidence threshold", None, &ollama_thresh_spin)); content.append(&ollama_group); // ── Calendar sync ──────────────────────────────────────────── - let cal_group = adw::preferences_group( - "Calendar Sync", - Some("Sync reminders to a Nextcloud calendar via CalDAV."), - ); + let (cal_group, cal_list) = field_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_enabled_switch = gtk4::Switch::builder().active(cfg.calendar.enabled).valign(gtk4::Align::Center).build(); + cal_list.append(&field_row("Enabled", None, &cal_enabled_switch)); - let cal_url_row = libadwaita::EntryRow::builder().title("Calendar URL").show_apply_button(true).build(); - cal_url_row.set_text(&cfg.calendar.url); - cal_group.add(&cal_url_row); + let cal_url_entry = text_entry(&cfg.calendar.url, 30); + cal_list.append(&field_row("Calendar URL", None, &cal_url_entry)); - let cal_user_row = libadwaita::EntryRow::builder().title("Username").show_apply_button(true).build(); - cal_user_row.set_text(&cfg.calendar.username); - cal_group.add(&cal_user_row); + let cal_user_entry = text_entry(&cfg.calendar.username, 16); + cal_list.append(&field_row("Username", None, &cal_user_entry)); - let cal_pass_row = libadwaita::PasswordEntryRow::builder().title("App password").build(); - cal_pass_row.set_text(&cfg.calendar.password); - cal_group.add(&cal_pass_row); + let cal_pass_entry = gtk4::PasswordEntry::builder().text(&cfg.calendar.password).show_peek_icon(true).valign(gtk4::Align::Center).css_classes(["field-input"]).build(); + cal_list.append(&field_row("App password", None, &cal_pass_entry)); content.append(&cal_group); // ── Status (instant-apply — no Save button) ───────────────── - let status_label = gtk4::Label::builder() - .label("") - .xalign(0.0) - .css_classes(["dim-label"]) - .margin_top(4) - .build(); + let status_label = gtk4::Label::builder().label("").xalign(0.0).css_classes(["dim-label"]).margin_top(4).build(); content.append(&status_label); // Reads every widget's current value and persists immediately. Every // control below calls this on its own "committed a change" signal - // (switch/combo/spin fire on change; entry rows fire on Enter or their - // apply-button, via show_apply_button) rather than a single Save button — - // AdwSwitchRow's whole design language implies changes take effect now. + // (switch/spin fire on change; entries fire on Enter or focus-out). let apply_now: Rc = Rc::new({ let selected_type = selected_type.clone(); - let ws_tag_row = ws_tag_row.clone(); - let archive_adj = archive_adj.clone(); - let snooze_row = snooze_row.clone(); - let morning_row = morning_row.clone(); - let grace_adj = grace_adj.clone(); - let model_path_row = model_path_row.clone(); - let tokenizer_row = tokenizer_row.clone(); - let ort_dylib_row = ort_dylib_row.clone(); - let ollama_enabled_row = ollama_enabled_row.clone(); - let ollama_endpoint_row = ollama_endpoint_row.clone(); - let ollama_model_row = ollama_model_row.clone(); - let ollama_thresh_adj = ollama_thresh_adj.clone(); - let cal_enabled_row = cal_enabled_row.clone(); - let cal_url_row = cal_url_row.clone(); - let cal_user_row = cal_user_row.clone(); - let cal_pass_row = cal_pass_row.clone(); + let ws_tag_switch = ws_tag_switch.clone(); + let archive_spin = archive_spin.clone(); + let snooze_entry = snooze_entry.clone(); + let morning_entry = morning_entry.clone(); + let grace_spin = grace_spin.clone(); + let model_path_entry = model_path_entry.clone(); + let tokenizer_entry = tokenizer_entry.clone(); + let ort_dylib_entry = ort_dylib_entry.clone(); + let ollama_enabled_switch = ollama_enabled_switch.clone(); + let ollama_endpoint_entry = ollama_endpoint_entry.clone(); + let ollama_model_entry = ollama_model_entry.clone(); + let ollama_thresh_spin = ollama_thresh_spin.clone(); + let cal_enabled_switch = cal_enabled_switch.clone(); + let cal_url_entry = cal_url_entry.clone(); + let cal_user_entry = cal_user_entry.clone(); + let cal_pass_entry = cal_pass_entry.clone(); let status_label = status_label.clone(); move || { let new_cfg = Config { settings: Settings { default_type: selected_type.borrow().clone(), - workspace_tag: ws_tag_row.is_active(), - snooze_options: snooze_row + workspace_tag: ws_tag_switch.is_active(), + snooze_options: snooze_entry .text() .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(), - archive_after_days: archive_adj.value() as i64, + archive_after_days: archive_spin.value() as i64, }, reminders: RemindersConfig { - default_morning: morning_row.text().to_string(), - missed_grace_minutes: grace_adj.value() as i64, + default_morning: morning_entry.text().to_string(), + missed_grace_minutes: grace_spin.value() as i64, }, model: ModelConfig { - path: model_path_row.text().to_string(), - tokenizer: tokenizer_row.text().to_string(), - ort_dylib_path: ort_dylib_row.text().to_string(), + path: model_path_entry.text().to_string(), + tokenizer: tokenizer_entry.text().to_string(), + ort_dylib_path: ort_dylib_entry.text().to_string(), ollama: OllamaConfig { - enabled: ollama_enabled_row.is_active(), - endpoint: ollama_endpoint_row.text().to_string(), - model: ollama_model_row.text().to_string(), - confidence_threshold: ollama_thresh_adj.value() as f32, + enabled: ollama_enabled_switch.is_active(), + endpoint: ollama_endpoint_entry.text().to_string(), + model: ollama_model_entry.text().to_string(), + confidence_threshold: ollama_thresh_spin.value() as f32, }, }, calendar: CalendarConfig { - enabled: cal_enabled_row.is_active(), - url: cal_url_row.text().to_string(), - username: cal_user_row.text().to_string(), - password: cal_pass_row.text().to_string(), + enabled: cal_enabled_switch.is_active(), + url: cal_url_entry.text().to_string(), + username: cal_user_entry.text().to_string(), + password: cal_pass_entry.text().to_string(), }, }; match new_cfg.save() { @@ -230,7 +252,7 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled } }); - // Switches / combo(-pills) / spinners apply the moment they change. + // Type pills, switches, spinners apply the moment they change. for (btn, name) in &type_pills { let apply_now = apply_now.clone(); let sel = selected_type.clone(); @@ -244,49 +266,49 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled }); } macro_rules! apply_on_active { - ($row:expr) => { + ($sw:expr) => { let apply_now = apply_now.clone(); - $row.connect_active_notify(move |_| apply_now()); + $sw.connect_state_set(move |_, _| { apply_now(); glib::Propagation::Proceed }); }; } - apply_on_active!(ws_tag_row); - apply_on_active!(ollama_enabled_row); - apply_on_active!(cal_enabled_row); + apply_on_active!(ws_tag_switch); + apply_on_active!(ollama_enabled_switch); + apply_on_active!(cal_enabled_switch); macro_rules! apply_on_value_changed { - ($adj:expr) => { + ($spin:expr) => { let apply_now = apply_now.clone(); - $adj.connect_value_changed(move |_| apply_now()); + $spin.connect_value_changed(move |_| apply_now()); }; } - apply_on_value_changed!(archive_adj); - apply_on_value_changed!(grace_adj); - apply_on_value_changed!(ollama_thresh_adj); - // Entry rows: `apply` fires on Enter or the inline apply-button - // (show_apply_button), which only appears once the text has actually - // changed — the standard libadwaita instant-apply text-field idiom. - macro_rules! apply_on_entry { - ($row:expr) => { - let apply_now = apply_now.clone(); - $row.connect_apply(move |_| apply_now()); - }; - } - apply_on_entry!(snooze_row); - apply_on_entry!(morning_row); - apply_on_entry!(model_path_row); - apply_on_entry!(tokenizer_row); - apply_on_entry!(ort_dylib_row); - apply_on_entry!(ollama_endpoint_row); - apply_on_entry!(ollama_model_row); - apply_on_entry!(cal_url_row); - apply_on_entry!(cal_user_row); - apply_on_entry!(cal_pass_row); + apply_on_value_changed!(archive_spin); + apply_on_value_changed!(grace_spin); + apply_on_value_changed!(ollama_thresh_spin); - let clamp = libadwaita::Clamp::builder() - .maximum_size(900) - .tightening_threshold(700) - .halign(gtk4::Align::Start) - .build(); - clamp.set_child(Some(&content)); + // Entries: apply on Enter, and on focus-out so a click-away doesn't + // silently discard the edit. + macro_rules! apply_on_entry { + ($entry:expr) => { + let apply_now_activate = apply_now.clone(); + $entry.connect_activate(move |_| apply_now_activate()); + let apply_now_focus = apply_now.clone(); + let focus = gtk4::EventControllerFocus::new(); + focus.connect_leave(move |_| apply_now_focus()); + $entry.add_controller(focus); + }; + } + apply_on_entry!(snooze_entry); + apply_on_entry!(morning_entry); + apply_on_entry!(model_path_entry); + apply_on_entry!(tokenizer_entry); + apply_on_entry!(ort_dylib_entry); + apply_on_entry!(ollama_endpoint_entry); + apply_on_entry!(ollama_model_entry); + apply_on_entry!(cal_url_entry); + apply_on_entry!(cal_user_entry); + apply_on_entry!(cal_pass_entry); + + content.set_halign(gtk4::Align::Start); + content.set_size_request(900, -1); let outer = gtk4::Box::builder() .orientation(gtk4::Orientation::Vertical) @@ -295,7 +317,7 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled .margin_top(12) .margin_bottom(16) .build(); - outer.append(&clamp); + outer.append(&content); scroll.set_child(Some(&outer)); scroll diff --git a/breadpad-shared/Cargo.toml b/breadpad-shared/Cargo.toml index 01470f9..72fe7b7 100644 --- a/breadpad-shared/Cargo.toml +++ b/breadpad-shared/Cargo.toml @@ -7,7 +7,7 @@ authors.workspace = true [dependencies] -bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.8", features = ["gtk"] } +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "dev", features = ["gtk"] } anyhow.workspace = true tracing.workspace = true serde.workspace = true diff --git a/breadpad-shared/src/theme.rs b/breadpad-shared/src/theme.rs index 6823934..e6fa63e 100644 --- a/breadpad-shared/src/theme.rs +++ b/breadpad-shared/src/theme.rs @@ -28,6 +28,27 @@ pub fn build_css(palette: &Palette, user_css: Option<&str>) -> String { /* breadpad/breadman-specific components */ window { border-radius: 8px; } +/* breadman/views/settings.rs — matches bos-settings' Row.svelte/ + NumberField.svelte/TextField.svelte exactly (same design tokens: 12/16px + row padding, transparent-at-rest input border, ch-width inputs) rather + than libadwaita's own AdwActionRow/AdwSpinRow padding and internal + spin-button sizing, which run noticeably taller/wider and don't expose a + way to constrain from the outside. `list.boxed-list` already gets its + surface fill + radius from the shared stylesheet; this only adds the + compact row padding and the divider between rows. */ +list.boxed-list row.field-row { padding: 12px 16px; min-height: 0; } +list.boxed-list row.field-row:not(:last-child) { border-bottom: 1px solid alpha(@on-surface, 0.08); } +.field-row-subtitle { opacity: 0.6; font-size: 12px; } + +.field-input { + background-color: @bg; + color: @on-surface; + border: 1px solid transparent; + border-radius: 6px; + padding: 4px 8px; +} +.field-input:focus-within { border-color: @accent; outline: none; } + .popup-entry { background: @bg; color: @fg;