Improve styling.

This commit is contained in:
Greyson Parrelli
2026-08-12 21:15:07 -04:00
parent 4e0e400372
commit e16b147e16
13 changed files with 1104 additions and 119 deletions
+141
View File
@@ -0,0 +1,141 @@
//! Which palette the app is painted with, and the plumbing that keeps three
//! separate colour systems agreeing about it.
//!
//! * **libadwaita's style manager** colours every stock widget — popovers,
//! entries, scrollbars, dialog chrome. It is told to force light or dark, or
//! left on `default` so it follows the desktop.
//! * **`style.css`** colours everything the app draws for itself. It is
//! written entirely against named colours, with one palette file per scheme;
//! the matching palette is prepended and the pair loaded as a single
//! provider.
//! * **`theme.zig`** holds the three fallback colours the terminal renderer
//! reads. Cairo never consults the style tree, so a CSS reload on its own
//! would leave every terminal grid painted in the palette it started with.
//!
//! The ordering that makes this work: the user's preference is only ever
//! *told to libadwaita*, and the repaint is driven by libadwaita's answer.
//! That is what makes "system" work at all — libadwaita already watches the
//! desktop for the setting and reports it as the `dark` property, so following
//! that property means a desktop that changes scheme at sunset repaints this
//! app too, with no extra machinery and no second source of truth.
const std = @import("std");
const adw = @import("adw");
const gdk = @import("gdk");
const gobject = @import("gobject");
const gtk = @import("gtk");
const Settings = @import("Settings.zig");
const theme = @import("theme.zig");
/// The stylesheet, once per scheme. Concatenated at compile time so that a
/// switch is a single `loadFromString` with no allocation and no file IO at
/// the moment the desktop changes its mind.
const style = @embedFile("style.css");
const css_dark = @embedFile("palette-dark.css") ++ style;
const css_light = @embedFile("palette-light.css") ++ style;
/// Called after the scheme changes, so the owner can repaint the things GTK
/// doesn't know are colour-dependent — the Cairo-drawn terminal grids.
pub const Callback = *const fn (ctx: ?*anyopaque) void;
var settings: Settings = .{};
var provider: ?*gtk.CssProvider = null;
var on_changed: ?Callback = null;
var on_changed_ctx: ?*anyopaque = null;
/// Load the saved preference, install the stylesheet, and start following the
/// resolved scheme. Called once, before the first window is built.
pub fn init() void {
settings = .load();
if (gdk.Display.getDefault()) |display| {
const css = gtk.CssProvider.new();
provider = css;
gtk.StyleContext.addProviderForDisplay(
display,
css.as(gtk.StyleProvider),
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
// Held for the life of the process, so there is nothing to disconnect.
_ = gobject.Object.signals.notify.connect(
adw.StyleManager.getDefault(),
?*anyopaque,
&onDarkChanged,
null,
.{ .detail = "dark" },
);
applyPreference();
}
pub fn currentTheme() Settings.Theme {
return settings.theme;
}
/// Change the preference, persist it, and repaint.
///
/// The write is best-effort: a settings file we can't write is worth a log
/// line, but it is not a reason to refuse the change for this session.
pub fn setTheme(to: Settings.Theme) void {
if (settings.theme == to) return;
settings.theme = to;
settings.save() catch {
std.log.err("failed to save settings", .{});
};
applyPreference();
}
/// Hand libadwaita the preference. Everything visible follows from its answer,
/// which arrives either synchronously — `set_color_scheme` settles `dark`
/// before it returns — or later, when the desktop changes under a `system`
/// preference.
fn applyPreference() void {
adw.StyleManager.getDefault().setColorScheme(switch (settings.theme) {
.system => .default,
.light => .force_light,
.dark => .force_dark,
});
// Called unconditionally rather than left to the notify handler: pinning
// to the scheme the desktop was already on doesn't change `dark` and so
// emits nothing, and the very first call has nothing to change either.
repaint();
}
fn onDarkChanged(
_: *adw.StyleManager,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
repaint();
}
/// Bring the stylesheet and the terminal palette to whatever libadwaita has
/// settled on. Idempotent, and cheap enough to call speculatively.
fn repaint() void {
const dark = adw.StyleManager.getDefault().getDark() != 0;
theme.setScheme(if (dark) .dark else .light);
if (provider) |css| css.loadFromString(if (dark) css_dark else css_light);
if (on_changed) |cb| cb(on_changed_ctx);
}
/// Register the repaint hook. There is one window, so there is one hook; a
/// second registration replaces the first rather than stacking.
pub fn onChanged(cb: Callback, ctx: ?*anyopaque) void {
on_changed = cb;
on_changed_ctx = ctx;
}
/// Drop the hook. Called as the window is torn down, so that a scheme change
/// arriving during shutdown can't reach freed state.
pub fn clearOnChanged() void {
on_changed = null;
on_changed_ctx = null;
}