dev #1
9 changed files with 128 additions and 45 deletions
|
|
@ -34,6 +34,12 @@ scale slider { background-color: @on-surface; border-radius: 999px; }\n\
|
|||
primary action instead of a dangerous one. Fixed regardless of palette. */\n\
|
||||
button.destructive-action { background-color: #c0392b; color: #ffffff; }\n\
|
||||
button.destructive-action:hover { background-color: #d64535; }\n\
|
||||
/* Adwaita's default switch slider (the knob) carries a box-shadow used for \
|
||||
its 3D bevel look — bread-theme's override only sets background-color, \
|
||||
so that shadow still renders as a pale ring around the knob on top of \
|
||||
our flat colour. */\n\
|
||||
switch slider { box-shadow: none; outline: none; border: none; background-image: none; }\n\
|
||||
switch { box-shadow: none; outline: none; border: none; background-image: none; }\n\
|
||||
";
|
||||
|
||||
thread_local! {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ pub fn build() -> GBox {
|
|||
|
||||
let (outer, c) = w::view_scaffold("Daemon");
|
||||
|
||||
c.append(&w::service_control("breadd.service"));
|
||||
c.append(&w::service_control("breadd.service", true, true));
|
||||
|
||||
c.append(&w::section("Daemon"));
|
||||
c.append(&w::dropdown_row(
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ pub fn build() -> GBox {
|
|||
let model = Rc::new(RefCell::new(read_contexts(&doc.borrow())));
|
||||
|
||||
let (outer, content) = w::view_scaffold("Launcher");
|
||||
content.append(&w::service_control("breadbox-sync.service"));
|
||||
content.append(&w::service_control("breadbox-sync.service", false, true));
|
||||
|
||||
content.append(&w::section("Contexts"));
|
||||
content.append(&w::hint(
|
||||
|
|
@ -157,10 +157,12 @@ pub fn build() -> GBox {
|
|||
scroll.set_child(Some(&list));
|
||||
content.append(&scroll);
|
||||
|
||||
let btn_row = GBox::new(Orientation::Horizontal, 8);
|
||||
btn_row.set_margin_top(8);
|
||||
|
||||
// Add-row button lives with the list it adds to, halign Start like
|
||||
// breadcrumbs' "Add network"/"Add profile" — Save stays alone in its
|
||||
// own row at the bottom, consistent with every other panel.
|
||||
let add_btn = Button::with_label("Add context");
|
||||
add_btn.set_halign(gtk4::Align::Start);
|
||||
add_btn.set_margin_top(8);
|
||||
{
|
||||
let model = model.clone();
|
||||
let list = list.clone();
|
||||
|
|
@ -172,12 +174,14 @@ pub fn build() -> GBox {
|
|||
rebuild_list(&list, &model);
|
||||
});
|
||||
}
|
||||
content.append(&add_btn);
|
||||
|
||||
let btn_row = GBox::new(Orientation::Horizontal, 12);
|
||||
btn_row.set_margin_top(16);
|
||||
let save_btn = Button::with_label("Save");
|
||||
save_btn.add_css_class("suggested-action");
|
||||
let status_lbl = Label::new(None);
|
||||
status_lbl.add_css_class("dim-label");
|
||||
|
||||
{
|
||||
let doc = doc.clone();
|
||||
let model = model.clone();
|
||||
|
|
@ -198,8 +202,6 @@ pub fn build() -> GBox {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
btn_row.append(&add_btn);
|
||||
btn_row.append(&save_btn);
|
||||
btn_row.append(&status_lbl);
|
||||
outer.append(&btn_row);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
//! had no presence in Settings at all despite running its own service.
|
||||
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::Box as GBox;
|
||||
use gtk4::{Box as GBox, Button};
|
||||
|
||||
use crate::ui::widgets as w;
|
||||
|
||||
|
|
@ -13,12 +13,21 @@ pub fn build() -> GBox {
|
|||
let (outer, c) = w::view_scaffold("Clipboard");
|
||||
|
||||
c.append(&w::hint(
|
||||
"breadclip keeps a history of copied text/images and shows it as a \
|
||||
popup (SUPER+V or SUPER+SHIFT+V). breadclipd is the background \
|
||||
daemon that actually watches the clipboard — breadclip itself is \
|
||||
just the popup UI, launched on demand.",
|
||||
"Keeps a history of copied text/images and shows it as a popup. \
|
||||
breadclipd (below) is the background daemon that watches the \
|
||||
clipboard — breadclip itself is just the popup UI, launched on \
|
||||
demand by the keybind or the button below.",
|
||||
));
|
||||
c.append(&w::service_control("breadclipd.service"));
|
||||
|
||||
let open_btn = Button::with_label("Open history (SUPER+V)");
|
||||
open_btn.add_css_class("suggested-action");
|
||||
open_btn.set_halign(gtk4::Align::Start);
|
||||
open_btn.connect_clicked(|_| {
|
||||
let _ = std::process::Command::new("breadclip").spawn();
|
||||
});
|
||||
c.append(&open_btn);
|
||||
|
||||
c.append(&w::service_control("breadclipd.service", false, false));
|
||||
|
||||
outer
|
||||
}
|
||||
|
|
|
|||
|
|
@ -335,7 +335,7 @@ pub fn build() -> GBox {
|
|||
let profiles = Rc::new(RefCell::new(read_profiles(&doc.borrow())));
|
||||
|
||||
let (outer, content) = w::view_scaffold("Wi-Fi Profiles");
|
||||
content.append(&w::service_control("breadcrumbs.service"));
|
||||
content.append(&w::service_control("breadcrumbs.service", false, true));
|
||||
|
||||
// [settings] — edited in place on the shared doc
|
||||
content.append(&w::section("Settings"));
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ pub fn build() -> GBox {
|
|||
let doc = Rc::new(RefCell::new(config::load_doc(&path)));
|
||||
|
||||
let (outer, c) = w::view_scaffold("File Search");
|
||||
c.append(&w::service_control("breadmill.service"));
|
||||
c.append(&w::service_control("breadmill.service", false, true));
|
||||
|
||||
c.append(&w::section("Power"));
|
||||
c.append(&w::hint(
|
||||
|
|
@ -110,10 +110,6 @@ pub fn build() -> GBox {
|
|||
200,
|
||||
));
|
||||
|
||||
c.append(&w::hint(
|
||||
"Changes take effect after a restart — use the Restart button above.",
|
||||
));
|
||||
|
||||
outer.append(&w::save_button(&doc, path));
|
||||
outer
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,18 +104,25 @@ pub fn build() -> GBox {
|
|||
|
||||
let list = ListBox::new();
|
||||
list.set_selection_mode(gtk4::SelectionMode::None);
|
||||
// Starts with an empty_state row ("Not scanned yet") rather than being
|
||||
// hidden — the "no results yet" and "scanned, found nothing" states are
|
||||
// the same idea and should look the same, not one a hint label and the
|
||||
// other an empty_state card.
|
||||
{
|
||||
let row = ListBoxRow::new();
|
||||
row.set_selectable(false);
|
||||
row.set_child(Some(&w::empty_state(
|
||||
"network-wireless-symbolic",
|
||||
"Not scanned yet",
|
||||
"Press Scan to see nearby networks.",
|
||||
)));
|
||||
list.append(&row);
|
||||
}
|
||||
let scroll = ScrolledWindow::new();
|
||||
scroll.set_min_content_height(220);
|
||||
scroll.set_child(Some(&list));
|
||||
// Hidden until a scan actually produces results — a 220px empty scroll
|
||||
// area between the radio row and the Scan button was dead space on
|
||||
// every fresh open of this panel.
|
||||
scroll.set_visible(false);
|
||||
content.append(&scroll);
|
||||
|
||||
let not_scanned = w::hint("Not scanned yet — press Scan to see nearby networks.");
|
||||
content.append(¬_scanned);
|
||||
|
||||
// Password entry, shown only while connecting to a secured, unknown
|
||||
// network — set visible/hidden rather than a modal dialog, to keep this
|
||||
// panel's async flow in one place instead of a nested dialog callback.
|
||||
|
|
@ -189,8 +196,6 @@ pub fn build() -> GBox {
|
|||
|
||||
let populate_list = {
|
||||
let list = list.clone();
|
||||
let scroll = scroll.clone();
|
||||
let not_scanned = not_scanned.clone();
|
||||
let pw_row = pw_row.clone();
|
||||
let pending_ssid = pending_ssid.clone();
|
||||
let connect_open_or_known = connect_open_or_known.clone();
|
||||
|
|
@ -198,8 +203,6 @@ pub fn build() -> GBox {
|
|||
while let Some(child) = list.first_child() {
|
||||
list.remove(&child);
|
||||
}
|
||||
not_scanned.set_visible(false);
|
||||
scroll.set_visible(true);
|
||||
if networks.is_empty() {
|
||||
let row = ListBoxRow::new();
|
||||
row.set_selectable(false);
|
||||
|
|
@ -280,6 +283,7 @@ pub fn build() -> GBox {
|
|||
};
|
||||
|
||||
let scan_btn = Button::with_label("Scan");
|
||||
scan_btn.set_halign(gtk4::Align::Start);
|
||||
{
|
||||
let status = status.clone();
|
||||
let populate_list = populate_list.clone();
|
||||
|
|
|
|||
|
|
@ -66,10 +66,9 @@ fn populate_packages(list: &ListBox, log_buf: >k4::TextBuffer, log_view: &Text
|
|||
let row = ListBoxRow::new();
|
||||
row.set_selectable(false);
|
||||
let hbox = GBox::new(Orientation::Horizontal, 16);
|
||||
hbox.set_margin_top(6);
|
||||
hbox.set_margin_bottom(6);
|
||||
hbox.set_margin_start(8);
|
||||
hbox.set_margin_end(8);
|
||||
hbox.add_css_class("card");
|
||||
hbox.set_margin_top(3);
|
||||
hbox.set_margin_bottom(3);
|
||||
|
||||
let name_lbl = Label::new(Some(name));
|
||||
name_lbl.set_hexpand(true);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use std::rc::Rc;
|
|||
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{
|
||||
Adjustment, Box as GBox, Button, DropDown, Entry, Expression, Label, Orientation,
|
||||
Adjustment, AlertDialog, Box as GBox, Button, DropDown, Entry, Expression, Label, Orientation,
|
||||
SpinButton, StringList, Switch,
|
||||
};
|
||||
use toml_edit::DocumentMut;
|
||||
|
|
@ -77,6 +77,13 @@ pub fn hint(text: &str) -> Label {
|
|||
lbl.add_css_class("dim-label");
|
||||
lbl.set_xalign(0.0);
|
||||
lbl.set_wrap(true);
|
||||
// set_wrap alone doesn't cap a label's *natural* width — a long enough
|
||||
// hint reports a natural width of its whole unwrapped string, which
|
||||
// `view_scaffold`'s content column (sized to its widest child, floored
|
||||
// not capped at CONTENT_MAX_WIDTH) then inherits, blowing the entire
|
||||
// panel full-width. This is what makes breadsearch/breadclip render
|
||||
// edge-to-edge while every other panel sits in a stable ~760px column.
|
||||
lbl.set_max_width_chars(64);
|
||||
lbl.set_margin_bottom(4);
|
||||
lbl
|
||||
}
|
||||
|
|
@ -111,6 +118,7 @@ pub fn empty_state(icon_name: &str, title: &str, hint_text: &str) -> GBox {
|
|||
hint_lbl.add_css_class("dim-label");
|
||||
hint_lbl.set_justify(gtk4::Justification::Center);
|
||||
hint_lbl.set_wrap(true);
|
||||
hint_lbl.set_max_width_chars(48);
|
||||
wrapper.append(&hint_lbl);
|
||||
|
||||
wrapper
|
||||
|
|
@ -126,18 +134,19 @@ pub fn view_scaffold(title: &str) -> (GBox, GBox) {
|
|||
let title_lbl = Label::new(Some(title));
|
||||
title_lbl.add_css_class("title");
|
||||
title_lbl.set_xalign(0.0);
|
||||
// Same width+center treatment as `content` below, so the title's left
|
||||
// edge lines up with the column it's heading instead of sitting flush
|
||||
// against the panel edge while the column centers ~440px to its right.
|
||||
title_lbl.set_halign(gtk4::Align::Center);
|
||||
title_lbl.set_size_request(CONTENT_MAX_WIDTH, -1);
|
||||
outer.append(&title_lbl);
|
||||
|
||||
let content = GBox::new(Orientation::Vertical, 4);
|
||||
// Explicit hexpand(false) overrides the auto-computed value GTK would
|
||||
// otherwise derive from the hexpand-ing labels inside every row, and
|
||||
// Start keeps content pinned under the left-aligned title instead of
|
||||
// stretching (Fill) or floating in the middle of a wide pane (Center).
|
||||
// Center distributes the dead space evenly either side on a wide window
|
||||
// instead of stranding it all on the right of a left-hugging column.
|
||||
content.set_hexpand(false);
|
||||
// Center rather than left-align: on a wide/maximized window, a
|
||||
// left-hugging column just moves the dead space from "right of every
|
||||
// row" to "right of the whole column" — centering distributes it evenly
|
||||
// either side instead, which reads as deliberate rather than stranded.
|
||||
content.set_halign(gtk4::Align::Center);
|
||||
content.set_size_request(CONTENT_MAX_WIDTH, -1);
|
||||
|
||||
|
|
@ -341,11 +350,29 @@ fn systemctl_active(unit: &str) -> bool {
|
|||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn systemctl_enabled(unit: &str) -> bool {
|
||||
std::process::Command::new("systemctl")
|
||||
.args(["--user", "is-enabled", "--quiet", unit])
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Live systemd `--user` unit status plus Start/Stop/Restart/Logs controls —
|
||||
/// every bread-ecosystem panel whose app is actually a daemon (not just a
|
||||
/// config file) gets this, instead of only ever being able to edit the TOML
|
||||
/// and hope the running process picks it up.
|
||||
pub fn service_control(unit: &'static str) -> GBox {
|
||||
///
|
||||
/// `critical` guards Stop behind a confirm dialog — for a unit like `breadd`
|
||||
/// (the whole desktop's event backbone), a bare click currently stops it as
|
||||
/// casually as an optional sync helper; everything else can stay one-click.
|
||||
///
|
||||
/// `has_config` adds a one-line note that Restart (not Save) is what applies
|
||||
/// config changes below — previously only breadsearch's panel said this,
|
||||
/// inconsistently, in its own hint text; bread/breadbox/breadcrumbs edit a
|
||||
/// running daemon's TOML and never mentioned the running process won't
|
||||
/// notice until restarted.
|
||||
pub fn service_control(unit: &'static str, critical: bool, has_config: bool) -> GBox {
|
||||
let wrapper = GBox::new(Orientation::Vertical, 4);
|
||||
wrapper.append(§ion("Service"));
|
||||
|
||||
|
|
@ -354,6 +381,10 @@ pub fn service_control(unit: &'static str) -> GBox {
|
|||
status_lbl.set_selectable(true);
|
||||
wrapper.append(&row(unit, &status_lbl));
|
||||
|
||||
let enabled_lbl = Label::new(None);
|
||||
enabled_lbl.add_css_class("dim-label");
|
||||
wrapper.append(&row("Starts at login", &enabled_lbl));
|
||||
|
||||
let btn_row = GBox::new(Orientation::Horizontal, 8);
|
||||
btn_row.set_margin_top(4);
|
||||
let toggle_btn = Button::new();
|
||||
|
|
@ -365,24 +396,53 @@ pub fn service_control(unit: &'static str) -> GBox {
|
|||
// plain closures aren't Clone.
|
||||
let refresh: Rc<dyn Fn()> = {
|
||||
let status_lbl = status_lbl.clone();
|
||||
let enabled_lbl = enabled_lbl.clone();
|
||||
let toggle_btn = toggle_btn.clone();
|
||||
Rc::new(move || {
|
||||
let active = systemctl_active(unit);
|
||||
status_lbl.set_text(if active { "Running" } else { "Stopped" });
|
||||
toggle_btn.set_label(if active { "Stop" } else { "Start" });
|
||||
enabled_lbl.set_text(if systemctl_enabled(unit) { "Yes" } else { "No" });
|
||||
})
|
||||
};
|
||||
refresh();
|
||||
|
||||
{
|
||||
let do_toggle: Rc<dyn Fn()> = {
|
||||
let refresh = refresh.clone();
|
||||
toggle_btn.connect_clicked(move |_| {
|
||||
Rc::new(move || {
|
||||
let verb = if systemctl_active(unit) { "stop" } else { "start" };
|
||||
let log_buf = gtk4::TextBuffer::new(None);
|
||||
let refresh = refresh.clone();
|
||||
stream_command_then(&["systemctl", "--user", verb, unit], log_buf, move || {
|
||||
refresh();
|
||||
});
|
||||
})
|
||||
};
|
||||
{
|
||||
let do_toggle = do_toggle.clone();
|
||||
toggle_btn.connect_clicked(move |btn| {
|
||||
if critical && systemctl_active(unit) {
|
||||
let window = btn.root().and_then(|r| r.downcast::<gtk4::Window>().ok());
|
||||
let dialog = AlertDialog::builder()
|
||||
.message(format!("Stop {unit}?"))
|
||||
.detail(
|
||||
"This is a core part of the desktop's event handling — \
|
||||
stopping it may affect other bread apps until it's \
|
||||
restarted.",
|
||||
)
|
||||
.buttons(["Cancel", "Stop"])
|
||||
.cancel_button(0)
|
||||
.default_button(0)
|
||||
.build();
|
||||
let do_toggle = do_toggle.clone();
|
||||
dialog.choose(window.as_ref(), gtk4::gio::Cancellable::NONE, move |result| {
|
||||
if result == Ok(1) {
|
||||
do_toggle();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
do_toggle();
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
|
|
@ -406,6 +466,13 @@ pub fn service_control(unit: &'static str) -> GBox {
|
|||
btn_row.append(&logs_btn);
|
||||
wrapper.append(&btn_row);
|
||||
|
||||
if has_config {
|
||||
wrapper.append(&hint(
|
||||
"Save (below) only writes the config file — click Restart above \
|
||||
for the running service to pick up the change.",
|
||||
));
|
||||
}
|
||||
|
||||
wrapper
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue