//! 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; }