Improve styling.

This commit is contained in:
Greyson Parrelli committed 2026-08-12 21:15:07 -04:00
1 parent 4e0e400372
commit e16b147e16
13 files changed
+1104 -119

No files matched your search

+197
View File
@@ -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);
}