Improve styling.
This commit is contained in:
1 parent
4e0e400372
commit
e16b147e16
13 files changed
+1104
-119
No files matched your search
@@ -0,0 +1,197 @@
|
||||
//! The settings page: preferences that apply to the whole app, grouped into
|
||||
//! titled sections.
|
||||
//!
|
||||
//! Built by hand in the same shape as the layout dialogs rather than out of
|
||||
//! `AdwPreferencesDialog`, so that it wears the app's own chrome — a stock
|
||||
//! preferences window in the middle of a window this heavily restyled reads as
|
||||
//! something that belongs to a different program.
|
||||
//!
|
||||
//! Changes apply the moment you make them, with no confirm button. That is the
|
||||
//! right shape for what is here: picking a scheme repaints the window behind
|
||||
//! the dialog, so the setting *is* its own preview, and an OK button would
|
||||
//! only offer to undo something you can see the result of.
|
||||
//!
|
||||
//! Only one is open at a time. The window is remembered while it is up and
|
||||
//! presented again rather than duplicated, since two settings pages
|
||||
//! disagreeing about which scheme is selected is a bug with no upside.
|
||||
|
||||
const std = @import("std");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const Settings = @import("Settings.zig");
|
||||
const appearance = @import("appearance.zig");
|
||||
|
||||
const SettingsDialog = @This();
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
window: *gtk.Window,
|
||||
|
||||
/// One per `Settings.Theme`, in declaration order, so the selected one can be
|
||||
/// re-checked without asking each button what it stands for.
|
||||
theme_buttons: [std.enums.values(Settings.Theme).len]*gtk.ToggleButton,
|
||||
|
||||
/// Set while a click is being applied, so that the resulting `toggled` signals
|
||||
/// on the other buttons in the group don't re-enter and undo it.
|
||||
updating: bool = false,
|
||||
|
||||
/// The open dialog, if there is one. A file-level singleton because "the
|
||||
/// settings page" is a singular thing from the user's point of view.
|
||||
var open: ?*SettingsDialog = null;
|
||||
|
||||
pub fn present(alloc: std.mem.Allocator, parent: *gtk.Window) !void {
|
||||
if (open) |existing| {
|
||||
existing.window.present();
|
||||
return;
|
||||
}
|
||||
|
||||
const self = try alloc.create(SettingsDialog);
|
||||
errdefer alloc.destroy(self);
|
||||
|
||||
self.* = .{
|
||||
.alloc = alloc,
|
||||
.window = gtk.Window.new(),
|
||||
.theme_buttons = undefined,
|
||||
};
|
||||
|
||||
self.window.setTitle("Settings");
|
||||
self.window.setTransientFor(parent);
|
||||
self.window.setModal(1);
|
||||
self.window.setDefaultSize(460, -1);
|
||||
self.window.as(gtk.Widget).addCssClass("playpen-dialog");
|
||||
|
||||
const content = gtk.Box.new(.vertical, 12);
|
||||
content.as(gtk.Widget).addCssClass("playpen-dialog-content");
|
||||
|
||||
content.append(self.buildAppearance());
|
||||
|
||||
const buttons = gtk.Box.new(.horizontal, 8);
|
||||
buttons.as(gtk.Widget).setHalign(.end);
|
||||
|
||||
// "Close" rather than "OK": nothing here is pending, so there is nothing
|
||||
// for a confirm button to confirm.
|
||||
const close = gtk.Button.newWithLabel("Close");
|
||||
_ = gtk.Button.signals.clicked.connect(close, *SettingsDialog, &onClose, self, .{});
|
||||
buttons.append(close.as(gtk.Widget));
|
||||
|
||||
content.append(buttons.as(gtk.Widget));
|
||||
self.window.setChild(content.as(gtk.Widget));
|
||||
|
||||
_ = gtk.Widget.signals.destroy.connect(
|
||||
self.window,
|
||||
*SettingsDialog,
|
||||
&onDestroy,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
open = self;
|
||||
self.window.present();
|
||||
}
|
||||
|
||||
/// The Appearance section. One group today; the box it returns is what a
|
||||
/// second section would sit next to.
|
||||
fn buildAppearance(self: *SettingsDialog) *gtk.Widget {
|
||||
const group = gtk.Box.new(.vertical, 10);
|
||||
group.as(gtk.Widget).addCssClass("playpen-settings-group");
|
||||
|
||||
const title = gtk.Label.new("Appearance");
|
||||
title.setXalign(0);
|
||||
title.as(gtk.Widget).addCssClass("playpen-settings-title");
|
||||
group.append(title.as(gtk.Widget));
|
||||
|
||||
const row = gtk.Box.new(.horizontal, 12);
|
||||
|
||||
const labels = gtk.Box.new(.vertical, 2);
|
||||
labels.as(gtk.Widget).setHexpand(1);
|
||||
labels.as(gtk.Widget).setValign(.center);
|
||||
|
||||
const name = gtk.Label.new("Theme");
|
||||
name.setXalign(0);
|
||||
name.as(gtk.Widget).addCssClass("playpen-dialog-label");
|
||||
labels.append(name.as(gtk.Widget));
|
||||
|
||||
const hint = gtk.Label.new("System follows the desktop's light or dark setting.");
|
||||
hint.setXalign(0);
|
||||
hint.setWrap(1);
|
||||
hint.as(gtk.Widget).addCssClass("playpen-dialog-sublabel");
|
||||
labels.append(hint.as(gtk.Widget));
|
||||
|
||||
row.append(labels.as(gtk.Widget));
|
||||
row.append(self.buildThemeChoice());
|
||||
|
||||
group.append(row.as(gtk.Widget));
|
||||
|
||||
return group.as(gtk.Widget);
|
||||
}
|
||||
|
||||
/// The scheme picker: one toggle per choice, drawn as a single linked control.
|
||||
///
|
||||
/// They are not put in a GTK radio group. Grouped toggles can't be unchecked by
|
||||
/// clicking the active one, which is the behaviour we want, but the group also
|
||||
/// emits `toggled` twice per change — once off, once on — and the off half
|
||||
/// would apply whichever scheme happened to be next in the list. Handling the
|
||||
/// exclusivity here instead makes each click exactly one decision.
|
||||
fn buildThemeChoice(self: *SettingsDialog) *gtk.Widget {
|
||||
const box = gtk.Box.new(.horizontal, 0);
|
||||
box.as(gtk.Widget).addCssClass("linked");
|
||||
box.as(gtk.Widget).addCssClass("playpen-settings-choice");
|
||||
box.as(gtk.Widget).setValign(.center);
|
||||
|
||||
const current = appearance.currentTheme();
|
||||
|
||||
for (std.enums.values(Settings.Theme), 0..) |value, i| {
|
||||
const button = gtk.ToggleButton.newWithLabel(value.label());
|
||||
button.setActive(@intFromBool(value == current));
|
||||
|
||||
_ = gtk.ToggleButton.signals.toggled.connect(
|
||||
button,
|
||||
*SettingsDialog,
|
||||
&onThemeToggled,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
box.append(button.as(gtk.Widget));
|
||||
self.theme_buttons[i] = button;
|
||||
}
|
||||
|
||||
return box.as(gtk.Widget);
|
||||
}
|
||||
|
||||
fn onThemeToggled(button: *gtk.ToggleButton, self: *SettingsDialog) callconv(.c) void {
|
||||
if (self.updating) return;
|
||||
|
||||
const values = std.enums.values(Settings.Theme);
|
||||
|
||||
const index = for (self.theme_buttons, 0..) |candidate, i| {
|
||||
if (candidate == button) break i;
|
||||
} else return;
|
||||
|
||||
// Clicking the choice you are already on: put it back rather than leaving
|
||||
// the picker with nothing selected, which would show a scheme is in force
|
||||
// that no button claims.
|
||||
if (button.getActive() == 0) {
|
||||
self.updating = true;
|
||||
defer self.updating = false;
|
||||
button.setActive(1);
|
||||
return;
|
||||
}
|
||||
|
||||
self.updating = true;
|
||||
defer self.updating = false;
|
||||
|
||||
for (self.theme_buttons, 0..) |other, i| {
|
||||
if (i != index) other.setActive(0);
|
||||
}
|
||||
|
||||
appearance.setTheme(values[index]);
|
||||
}
|
||||
|
||||
fn onClose(_: *gtk.Button, self: *SettingsDialog) callconv(.c) void {
|
||||
self.window.destroy();
|
||||
}
|
||||
|
||||
fn onDestroy(_: *gtk.Window, self: *SettingsDialog) callconv(.c) void {
|
||||
if (open == self) open = null;
|
||||
self.alloc.destroy(self);
|
||||
}
|
||||
Reference in new issue
Block a user