Improve styling.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user