720 lines
26 KiB
Zig
720 lines
26 KiB
Zig
//! The color editor: one swatch per name in the palette, for the scheme that
|
|
//! is on screen.
|
|
//!
|
|
//! It edits **the palette you can see**, and that is the whole design. Every
|
|
//! color here paints something behind this window, so a change is its own
|
|
//! preview — pick a sidebar color and the sidebar is that color before the
|
|
//! dialog has closed. Editing the *other* scheme's palette from here would mean
|
|
//! a page full of swatches whose effect you have to take on faith, so the
|
|
//! answer to "I want to change the light one" is the theme picker above: switch
|
|
//! to light, and this section switches with it.
|
|
//!
|
|
//! Overrides are stored per color rather than as a whole palette, which is what
|
|
//! lets a row say whether it has been changed and put itself back, and what lets
|
|
//! the defaults be retuned in a later version for everyone who never touched
|
|
//! them. The terminal's colors go one step further: left alone they *follow*
|
|
//! the surface and text colors above them (see `palette.Key.inherits`), so
|
|
//! recoloring the app recolors the terminal drawn on it without asking anyone
|
|
//! to set the same color twice — which is also why every row is re-read after
|
|
//! any change, not just the row that changed.
|
|
//!
|
|
//! The swatches start collapsed, and the two controls above them do not. That
|
|
//! split is the point of the base color: thirty-nine swatches is a fair way to
|
|
//! *correct* a theme and a miserable way to *choose* one, so the path that is
|
|
//! always on offer is "pick a color, get a palette" (`tint.zig`), and the
|
|
//! wall of swatches is behind a disclosure for the times that isn't enough. The
|
|
//! two compose — a swatch that has been set by hand outranks the generated
|
|
//! color underneath it — which is why picking a new base does not clear the
|
|
//! corrections someone made to the last one.
|
|
|
|
const std = @import("std");
|
|
const gdk = @import("gdk");
|
|
const gobject = @import("gobject");
|
|
const gtk = @import("gtk");
|
|
|
|
const Settings = @import("Settings.zig");
|
|
const appearance = @import("appearance.zig");
|
|
const palette = @import("palette.zig");
|
|
const theme = @import("theme.zig");
|
|
|
|
const PaletteEditor = @This();
|
|
|
|
/// How the editor reports a settings file it couldn't write. The settings page
|
|
/// owns the label it goes in, since the startup list below reports the same
|
|
/// failure the same way.
|
|
pub const Report = *const fn (ctx: ?*anyopaque, message: ?[:0]const u8) void;
|
|
|
|
pub const Options = struct {
|
|
on_report: Report,
|
|
ctx: ?*anyopaque = null,
|
|
};
|
|
|
|
/// One color: its swatch, and — for the ones listed as rows rather than packed
|
|
/// into the ANSI grid — the hex it currently resolves to and the button that
|
|
/// puts it back.
|
|
const Row = struct {
|
|
editor: *PaletteEditor,
|
|
key: palette.Key,
|
|
swatch: *gtk.ColorDialogButton,
|
|
hex: ?*gtk.Label = null,
|
|
revert: ?*gtk.Button = null,
|
|
};
|
|
|
|
/// One section heading, kept only so its reset button can be grayed out when
|
|
/// the section has nothing to reset.
|
|
const GroupHeader = struct {
|
|
editor: *PaletteEditor,
|
|
group: palette.Group,
|
|
reset: *gtk.Button,
|
|
};
|
|
|
|
const group_count = std.enums.values(palette.Group).len;
|
|
|
|
alloc: std.mem.Allocator,
|
|
|
|
/// The whole section: the disclosure row, and the swatches under it.
|
|
root: *gtk.Box,
|
|
|
|
/// Everything the toggle shows and hides.
|
|
body: *gtk.Box,
|
|
|
|
/// Says which scheme is being edited, and is rewritten when that changes.
|
|
scheme_hint: *gtk.Label,
|
|
|
|
/// The color the rest of the palette is generated from, the hex it currently
|
|
/// stands at, and the button that drops it. Assigned in `build`, since the
|
|
/// swatch has to be handed its dialog at construction.
|
|
base: *gtk.ColorDialogButton,
|
|
base_hex: *gtk.Label,
|
|
base_clear: *gtk.Button,
|
|
|
|
/// How far apart to spread the colors built from the base. Insensitive until
|
|
/// there is a base, since on its own it has nothing to spread.
|
|
contrast: *gtk.Scale,
|
|
|
|
/// Puts the whole scheme back to its defaults.
|
|
reset: *gtk.Button,
|
|
|
|
/// Indexed by `@intFromEnum(key)`, so a row is found without searching. Inline
|
|
/// in the struct rather than allocated: there is exactly one row per color, the
|
|
/// count is known at compile time, and the addresses have to be stable because
|
|
/// every swatch's callback holds one.
|
|
rows: [palette.count]Row,
|
|
groups: [group_count]GroupHeader,
|
|
|
|
/// Set while the rows are being written from the settings, so that the
|
|
/// `notify::rgba` each `setRgba` provokes doesn't come back in as an edit.
|
|
updating: bool = false,
|
|
|
|
on_report: Report,
|
|
ctx: ?*anyopaque,
|
|
|
|
pub fn create(alloc: std.mem.Allocator, opts: Options) !*PaletteEditor {
|
|
const self = try alloc.create(PaletteEditor);
|
|
errdefer alloc.destroy(self);
|
|
|
|
self.* = .{
|
|
.alloc = alloc,
|
|
.root = gtk.Box.new(.vertical, 10),
|
|
.body = gtk.Box.new(.vertical, 14),
|
|
.scheme_hint = gtk.Label.new(null),
|
|
.base = undefined,
|
|
.base_hex = gtk.Label.new(null),
|
|
.base_clear = gtk.Button.newFromIconName("edit-undo-symbolic"),
|
|
// The step is what the arrow keys move by; the range is the one
|
|
// `palette.Tint.contrast` is defined over.
|
|
.contrast = gtk.Scale.newWithRange(.horizontal, -1, 1, 0.05),
|
|
.reset = gtk.Button.newWithLabel("Reset every color"),
|
|
.rows = undefined,
|
|
.groups = undefined,
|
|
.on_report = opts.on_report,
|
|
.ctx = opts.ctx,
|
|
};
|
|
|
|
self.build();
|
|
self.reload();
|
|
return self;
|
|
}
|
|
|
|
/// Free the editor's own memory. The widgets belong to the window that is being
|
|
/// torn down around them, so there is nothing to destroy here.
|
|
pub fn destroy(self: *PaletteEditor) void {
|
|
self.alloc.destroy(self);
|
|
}
|
|
|
|
pub fn widget(self: *PaletteEditor) *gtk.Widget {
|
|
return self.root.as(gtk.Widget);
|
|
}
|
|
|
|
/// Re-read every swatch from the settings.
|
|
///
|
|
/// Called by the settings page when the theme changes, because the scheme being
|
|
/// edited changed with it — the same rows now stand for the other palette.
|
|
pub fn refresh(self: *PaletteEditor) void {
|
|
self.reload();
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Building
|
|
|
|
fn build(self: *PaletteEditor) void {
|
|
self.root.append(self.buildHeader());
|
|
|
|
// Above the base color rather than inside the disclosure with the
|
|
// swatches, because it is true of the base color too: both palettes can be
|
|
// given one, and this is the only thing on the page that says which of them
|
|
// the control below is about to change.
|
|
self.scheme_hint.setXalign(0);
|
|
self.scheme_hint.setWrap(1);
|
|
self.scheme_hint.as(gtk.Widget).addCssClass("playpen-dialog-hint");
|
|
self.root.append(self.scheme_hint.as(gtk.Widget));
|
|
|
|
self.root.append(self.buildBase());
|
|
|
|
self.buildGroups();
|
|
|
|
self.reset.as(gtk.Widget).addCssClass("flat");
|
|
self.reset.as(gtk.Widget).setHalign(.start);
|
|
self.reset.as(gtk.Widget).setTooltipText(
|
|
"Put every color in this scheme back to the one Playpen ships with, " ++
|
|
"and drop the base color above",
|
|
);
|
|
_ = gtk.Button.signals.clicked.connect(self.reset, *PaletteEditor, &onReset, self, .{});
|
|
self.body.append(self.reset.as(gtk.Widget));
|
|
|
|
// Collapsed until asked for; see the note at the top of the file.
|
|
self.body.as(gtk.Widget).setVisible(0);
|
|
self.root.append(self.body.as(gtk.Widget));
|
|
}
|
|
|
|
/// The disclosure row, in the same shape as the theme row above it: what it is
|
|
/// on the left, the control on the right.
|
|
fn buildHeader(self: *PaletteEditor) *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("Palette");
|
|
name.setXalign(0);
|
|
name.as(gtk.Widget).addCssClass("playpen-dialog-label");
|
|
labels.append(name.as(gtk.Widget));
|
|
|
|
const sub = gtk.Label.new("Every color the window and the terminal are painted from.");
|
|
sub.setXalign(0);
|
|
sub.setWrap(1);
|
|
sub.as(gtk.Widget).addCssClass("playpen-dialog-sublabel");
|
|
labels.append(sub.as(gtk.Widget));
|
|
|
|
row.append(labels.as(gtk.Widget));
|
|
|
|
// Wrapped rather than styled directly: the settings page's button styling
|
|
// hangs off the container, so this comes out looking like one of the theme
|
|
// buttons beside it rather than like a stock Adwaita button.
|
|
const wrap = gtk.Box.new(.horizontal, 0);
|
|
wrap.as(gtk.Widget).addCssClass("linked");
|
|
wrap.as(gtk.Widget).addCssClass("playpen-settings-choice");
|
|
wrap.as(gtk.Widget).setValign(.center);
|
|
|
|
const toggle = gtk.ToggleButton.newWithLabel("Customize");
|
|
_ = gtk.ToggleButton.signals.toggled.connect(
|
|
toggle,
|
|
*PaletteEditor,
|
|
&onDisclosureToggled,
|
|
self,
|
|
.{},
|
|
);
|
|
wrap.append(toggle.as(gtk.Widget));
|
|
|
|
row.append(wrap.as(gtk.Widget));
|
|
|
|
return row.as(gtk.Widget);
|
|
}
|
|
|
|
/// The two controls that are always on offer: the color the palette is built
|
|
/// out of, and how far apart to spread what gets built.
|
|
///
|
|
/// Outside the disclosure below on purpose. This is the answer for someone who
|
|
/// wants the app to be green, and the thirty-nine swatches are the answer for
|
|
/// someone who has already got there and wants the "waiting on you" amber a
|
|
/// shade warmer — putting the first behind the same toggle as the second would
|
|
/// hide the easy path behind the hard one.
|
|
fn buildBase(self: *PaletteEditor) *gtk.Widget {
|
|
const group = gtk.Box.new(.vertical, 6);
|
|
|
|
// The same class the swatch sections below carry, which is where the
|
|
// stylesheet hangs the "a color button is a rectangle of the color, not a
|
|
// button holding one" rule. This row has a color button in it and wants to
|
|
// look like the ones under Customize.
|
|
group.as(gtk.Widget).addCssClass("playpen-color-group");
|
|
|
|
// ---- the color ----
|
|
const row = gtk.Box.new(.horizontal, 8);
|
|
row.as(gtk.Widget).addCssClass("playpen-color-row");
|
|
|
|
const label = gtk.Label.new("Base color");
|
|
label.setXalign(0);
|
|
label.as(gtk.Widget).setHexpand(1);
|
|
label.as(gtk.Widget).addCssClass("playpen-dialog-label");
|
|
row.append(label.as(gtk.Widget));
|
|
|
|
self.base_hex.as(gtk.Widget).addCssClass("playpen-color-hex");
|
|
row.append(self.base_hex.as(gtk.Widget));
|
|
|
|
const dialog = gtk.ColorDialog.new();
|
|
dialog.setWithAlpha(0);
|
|
dialog.setModal(1);
|
|
dialog.setTitle("Base color");
|
|
|
|
self.base = gtk.ColorDialogButton.new(dialog);
|
|
self.base.as(gtk.Widget).setValign(.center);
|
|
self.base.as(gtk.Widget).setSizeRequest(52, 24);
|
|
_ = gobject.Object.signals.notify.connect(
|
|
self.base,
|
|
*PaletteEditor,
|
|
&onBasePicked,
|
|
self,
|
|
.{ .detail = "rgba" },
|
|
);
|
|
row.append(self.base.as(gtk.Widget));
|
|
|
|
self.base_clear.as(gtk.Widget).addCssClass("flat");
|
|
self.base_clear.as(gtk.Widget).setTooltipText("Back to Playpen's own colors");
|
|
_ = gtk.Button.signals.clicked.connect(
|
|
self.base_clear,
|
|
*PaletteEditor,
|
|
&onBaseCleared,
|
|
self,
|
|
.{},
|
|
);
|
|
row.append(self.base_clear.as(gtk.Widget));
|
|
|
|
group.append(row.as(gtk.Widget));
|
|
group.append(hint(
|
|
"Pick one and the rest follows: a very dark version of it behind the window, " ++
|
|
"lighter ones for the panes and the text on them, and the color itself as the accent. " ++
|
|
"Anything you set under Customize stays where you put it.",
|
|
));
|
|
|
|
// ---- how far apart ----
|
|
const contrast_row = gtk.Box.new(.horizontal, 8);
|
|
contrast_row.as(gtk.Widget).addCssClass("playpen-color-row");
|
|
contrast_row.as(gtk.Widget).setTooltipText(
|
|
"How far apart to spread the colors built from the base.\n" ++
|
|
"Starker pulls the backdrop darker and the text brighter; softer draws them together.",
|
|
);
|
|
|
|
const contrast_label = gtk.Label.new("Contrast");
|
|
contrast_label.setXalign(0);
|
|
contrast_label.as(gtk.Widget).addCssClass("playpen-dialog-label");
|
|
contrast_row.append(contrast_label.as(gtk.Widget));
|
|
|
|
self.contrast.as(gtk.Widget).setHexpand(1);
|
|
self.contrast.as(gtk.Widget).addCssClass("playpen-contrast");
|
|
|
|
// No fill behind the handle. GTK draws one from the low end of the range,
|
|
// which would say the setting counts up from "soft"; it counts out from the
|
|
// middle, and the mark there is what says so.
|
|
self.contrast.setHasOrigin(0);
|
|
|
|
// No number beside it: the value is a position on a scale with no unit, and
|
|
// "0.35" says less about what it will look like than the slider already does.
|
|
self.contrast.setDrawValue(0);
|
|
self.contrast.as(gtk.Range).setIncrements(0.05, 0.25);
|
|
self.contrast.as(gtk.Range).setRoundDigits(2);
|
|
|
|
// The unlabeled middle mark is what makes the shipped spacing findable
|
|
// again after a drag: GTK snaps the handle to a mark it passes near.
|
|
self.contrast.addMark(-1, .bottom, "Soft");
|
|
self.contrast.addMark(0, .bottom, null);
|
|
self.contrast.addMark(1, .bottom, "Stark");
|
|
|
|
_ = gtk.Range.signals.value_changed.connect(
|
|
self.contrast,
|
|
*PaletteEditor,
|
|
&onContrastChanged,
|
|
self,
|
|
.{},
|
|
);
|
|
contrast_row.append(self.contrast.as(gtk.Widget));
|
|
|
|
group.append(contrast_row.as(gtk.Widget));
|
|
|
|
return group.as(gtk.Widget);
|
|
}
|
|
|
|
/// One section per group, in the order the keys are declared: the surfaces, the
|
|
/// text on them, the accents, and the terminal last.
|
|
fn buildGroups(self: *PaletteEditor) void {
|
|
var current: ?palette.Group = null;
|
|
var section: *gtk.Box = undefined;
|
|
|
|
// The ANSI colors are a grid of bare swatches rather than sixteen labeled
|
|
// rows: they are a palette people recognize by position — eight normal, then
|
|
// eight bright — and a list of their names says less than the strip does.
|
|
var ansi: *gtk.Grid = undefined;
|
|
|
|
for (std.enums.values(palette.Key)) |key| {
|
|
const group = key.group();
|
|
|
|
if (current == null or current.? != group) {
|
|
current = group;
|
|
section = gtk.Box.new(.vertical, 6);
|
|
section.as(gtk.Widget).addCssClass("playpen-color-group");
|
|
section.append(self.buildGroupHeader(group));
|
|
if (group.hint()) |text| section.append(hint(text));
|
|
|
|
if (group == .ansi) {
|
|
ansi = gtk.Grid.new();
|
|
ansi.setRowSpacing(4);
|
|
ansi.setColumnSpacing(4);
|
|
ansi.as(gtk.Widget).setHalign(.start);
|
|
section.append(ansi.as(gtk.Widget));
|
|
}
|
|
|
|
self.body.append(section.as(gtk.Widget));
|
|
}
|
|
|
|
if (key.ansiIndex()) |i| {
|
|
const swatch = self.newSwatch(key, 30, 22);
|
|
ansi.attach(swatch.as(gtk.Widget), @intCast(i % 8), @intCast(i / 8), 1, 1);
|
|
} else {
|
|
section.append(self.buildRow(key));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn buildGroupHeader(self: *PaletteEditor, group: palette.Group) *gtk.Widget {
|
|
const row = gtk.Box.new(.horizontal, 6);
|
|
|
|
const title = gtk.Label.new(group.label());
|
|
title.setXalign(0);
|
|
title.as(gtk.Widget).setHexpand(1);
|
|
title.as(gtk.Widget).addCssClass("playpen-color-group-title");
|
|
row.append(title.as(gtk.Widget));
|
|
|
|
const reset = gtk.Button.newFromIconName("edit-undo-symbolic");
|
|
reset.as(gtk.Widget).addCssClass("flat");
|
|
reset.as(gtk.Widget).setTooltipText("Reset the colors in this section");
|
|
|
|
const header = &self.groups[@intFromEnum(group)];
|
|
header.* = .{ .editor = self, .group = group, .reset = reset };
|
|
_ = gtk.Button.signals.clicked.connect(reset, *GroupHeader, &onResetGroup, header, .{});
|
|
|
|
row.append(reset.as(gtk.Widget));
|
|
|
|
return row.as(gtk.Widget);
|
|
}
|
|
|
|
/// One color as a row: what it is, what it currently is, and the two controls
|
|
/// for changing that.
|
|
fn buildRow(self: *PaletteEditor, key: palette.Key) *gtk.Widget {
|
|
const row = gtk.Box.new(.horizontal, 8);
|
|
row.as(gtk.Widget).addCssClass("playpen-color-row");
|
|
|
|
const label = gtk.Label.new(key.label());
|
|
label.setXalign(0);
|
|
label.as(gtk.Widget).setHexpand(1);
|
|
label.as(gtk.Widget).addCssClass("playpen-dialog-label");
|
|
row.append(label.as(gtk.Widget));
|
|
|
|
// The tooltip carries what the color is *for*, plus the name it goes by in
|
|
// the settings file, since anyone hand-editing that file is looking at this
|
|
// list to find out what to call things.
|
|
var tip: [256]u8 = undefined;
|
|
const tooltip: [:0]const u8 = std.fmt.bufPrintZ(&tip, "{s}{s}{s}", .{
|
|
key.hint() orelse "",
|
|
if (key.hint() != null) "\n" else "",
|
|
@tagName(key),
|
|
}) catch @tagName(key);
|
|
row.as(gtk.Widget).setTooltipText(tooltip);
|
|
|
|
const hex = gtk.Label.new(null);
|
|
hex.as(gtk.Widget).addCssClass("playpen-color-hex");
|
|
row.append(hex.as(gtk.Widget));
|
|
|
|
const swatch = self.newSwatch(key, 52, 24);
|
|
row.append(swatch.as(gtk.Widget));
|
|
|
|
const revert = gtk.Button.newFromIconName("edit-undo-symbolic");
|
|
revert.as(gtk.Widget).addCssClass("flat");
|
|
revert.as(gtk.Widget).setTooltipText("Back to the default");
|
|
|
|
const entry = &self.rows[@intFromEnum(key)];
|
|
entry.hex = hex;
|
|
entry.revert = revert;
|
|
_ = gtk.Button.signals.clicked.connect(revert, *Row, &onRevert, entry, .{});
|
|
|
|
row.append(revert.as(gtk.Widget));
|
|
|
|
return row.as(gtk.Widget);
|
|
}
|
|
|
|
/// A swatch, and the row record that goes with it.
|
|
///
|
|
/// Alpha is off: `style.css` derives every wash it needs from `alpha()` on the
|
|
/// color it belongs to, so a translucent palette entry would be a second way
|
|
/// of saying the same thing, and one the terminal renderer could not honor.
|
|
fn newSwatch(
|
|
self: *PaletteEditor,
|
|
key: palette.Key,
|
|
width: c_int,
|
|
height: c_int,
|
|
) *gtk.ColorDialogButton {
|
|
const dialog = gtk.ColorDialog.new();
|
|
dialog.setWithAlpha(0);
|
|
dialog.setModal(1);
|
|
dialog.setTitle(key.label());
|
|
|
|
const swatch = gtk.ColorDialogButton.new(dialog);
|
|
swatch.as(gtk.Widget).setValign(.center);
|
|
swatch.as(gtk.Widget).setSizeRequest(width, height);
|
|
swatch.as(gtk.Widget).setTooltipText(key.label());
|
|
|
|
const entry = &self.rows[@intFromEnum(key)];
|
|
entry.* = .{ .editor = self, .key = key, .swatch = swatch };
|
|
|
|
_ = gobject.Object.signals.notify.connect(
|
|
swatch,
|
|
*Row,
|
|
&onColorPicked,
|
|
entry,
|
|
.{ .detail = "rgba" },
|
|
);
|
|
|
|
return swatch;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Reading the settings back into the rows
|
|
|
|
fn reload(self: *PaletteEditor) void {
|
|
self.updating = true;
|
|
defer self.updating = false;
|
|
|
|
const scheme = theme.currentScheme();
|
|
const changes = Settings.get().colors.of(scheme);
|
|
|
|
var buf: [192]u8 = undefined;
|
|
const heading: [:0]const u8 = std.fmt.bufPrintZ(
|
|
&buf,
|
|
"Editing the {s} palette, the one on screen — changes apply as you make them. " ++
|
|
"Switch the theme above to edit the other.",
|
|
.{scheme.label()},
|
|
) catch "Changes apply as you make them.";
|
|
self.scheme_hint.setText(heading);
|
|
|
|
// With no base of its own, the swatch shows the accent this scheme is
|
|
// already painted with — so the first click on it starts from the theme in
|
|
// front of you rather than from whatever color a picker opens on.
|
|
const base = if (changes.tint) |tint|
|
|
tint.base
|
|
else
|
|
palette.resolve(.accent, scheme, changes);
|
|
|
|
var base_rgba = toRgba(base);
|
|
self.base.setRgba(&base_rgba);
|
|
|
|
var base_hex: [7:0]u8 = undefined;
|
|
self.base_hex.setText(base.hex(&base_hex));
|
|
self.base_clear.as(gtk.Widget).setSensitive(@intFromBool(changes.tint != null));
|
|
|
|
self.contrast.as(gtk.Range).setValue(if (changes.tint) |tint| tint.contrast else 0);
|
|
self.contrast.as(gtk.Widget).setSensitive(@intFromBool(changes.tint != null));
|
|
|
|
for (&self.rows) |*row| {
|
|
// The color, not the override: a terminal color that is following the
|
|
// surface above it, or any color at all under a base, has to show the
|
|
// color it is actually painted in.
|
|
const color = palette.resolve(row.key, scheme, changes);
|
|
|
|
var rgba = toRgba(color);
|
|
row.swatch.setRgba(&rgba);
|
|
|
|
if (row.hex) |label| {
|
|
var hex_buf: [7:0]u8 = undefined;
|
|
label.setText(color.hex(&hex_buf));
|
|
}
|
|
|
|
// Whether there is anything to revert *to*, which is a different
|
|
// question: neither an inherited color nor a generated one is a color
|
|
// that has been set.
|
|
if (row.revert) |button| {
|
|
button.as(gtk.Widget).setSensitive(@intFromBool(changes.overrides.get(row.key) != null));
|
|
}
|
|
}
|
|
|
|
for (&self.groups) |*header| {
|
|
header.reset.as(gtk.Widget).setSensitive(
|
|
@intFromBool(groupIsSet(header.group, &changes.overrides)),
|
|
);
|
|
}
|
|
|
|
self.reset.as(gtk.Widget).setSensitive(@intFromBool(changes.anySet()));
|
|
}
|
|
|
|
fn groupIsSet(group: palette.Group, overrides: *const palette.Overrides) bool {
|
|
for (std.enums.values(palette.Key)) |key| {
|
|
if (key.group() == group and overrides.get(key) != null) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Show the change, then write it down.
|
|
///
|
|
/// In that order on purpose: repainting is what the user asked for and cannot
|
|
/// fail, while the write can — and a settings file that couldn't be written is
|
|
/// worth saying so about without also refusing the color for this session.
|
|
fn applied(self: *PaletteEditor) void {
|
|
appearance.refresh();
|
|
self.reload();
|
|
|
|
Settings.get().save() catch {
|
|
self.on_report(self.ctx, "Could not write the settings file.");
|
|
return;
|
|
};
|
|
self.on_report(self.ctx, null);
|
|
}
|
|
|
|
/// Show the change and leave the file for later.
|
|
///
|
|
/// For the contrast slider alone. Every other control here settles on a value
|
|
/// once — a color dialog reports when it is dismissed, a button when it is
|
|
/// clicked — but a slider reports on every pixel of a drag, and writing the
|
|
/// settings file at that rate would be an fsync per frame of an animation. The
|
|
/// settings page writes on close, so nothing is lost by waiting; see the note at
|
|
/// the top of `SettingsDialog`.
|
|
fn appliedLive(self: *PaletteEditor) void {
|
|
appearance.refresh();
|
|
self.reload();
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Handlers
|
|
|
|
fn onDisclosureToggled(button: *gtk.ToggleButton, self: *PaletteEditor) callconv(.c) void {
|
|
self.body.as(gtk.Widget).setVisible(button.getActive());
|
|
}
|
|
|
|
fn onColorPicked(
|
|
swatch: *gtk.ColorDialogButton,
|
|
_: *gobject.ParamSpec,
|
|
row: *Row,
|
|
) callconv(.c) void {
|
|
const self = row.editor;
|
|
if (self.updating) return;
|
|
|
|
// The scheme is read here rather than remembered from the last reload: a
|
|
// desktop that flips to light under a `system` preference while this page is
|
|
// open changes which palette an edit belongs to, and writing to the one the
|
|
// rows were last filled from would put the color in the palette that isn't
|
|
// on screen.
|
|
const scheme = theme.currentScheme();
|
|
Settings.get().colors.of(scheme).overrides.set(row.key, fromRgba(swatch.getRgba()));
|
|
|
|
self.applied();
|
|
}
|
|
|
|
fn onRevert(_: *gtk.Button, row: *Row) callconv(.c) void {
|
|
const self = row.editor;
|
|
Settings.get().colors.of(theme.currentScheme()).overrides.set(row.key, null);
|
|
self.applied();
|
|
}
|
|
|
|
fn onResetGroup(_: *gtk.Button, header: *GroupHeader) callconv(.c) void {
|
|
const self = header.editor;
|
|
const changes = Settings.get().colors.of(theme.currentScheme());
|
|
|
|
for (std.enums.values(palette.Key)) |key| {
|
|
if (key.group() == header.group) changes.overrides.set(key, null);
|
|
}
|
|
|
|
self.applied();
|
|
}
|
|
|
|
fn onReset(_: *gtk.Button, self: *PaletteEditor) callconv(.c) void {
|
|
Settings.get().colors.of(theme.currentScheme()).* = .{};
|
|
self.applied();
|
|
}
|
|
|
|
fn onBasePicked(
|
|
swatch: *gtk.ColorDialogButton,
|
|
_: *gobject.ParamSpec,
|
|
self: *PaletteEditor,
|
|
) callconv(.c) void {
|
|
if (self.updating) return;
|
|
|
|
// The scheme is read here rather than remembered, for the reason
|
|
// `onColorPicked` gives.
|
|
const changes = Settings.get().colors.of(theme.currentScheme());
|
|
changes.tint = .{
|
|
.base = fromRgba(swatch.getRgba()),
|
|
// Carried over rather than reset: trying a second base against a spread
|
|
// you have already settled on is the common move, and having the slider
|
|
// jump back to the middle every time would make it impossible.
|
|
.contrast = if (changes.tint) |tint| tint.contrast else 0,
|
|
};
|
|
|
|
self.applied();
|
|
}
|
|
|
|
fn onBaseCleared(_: *gtk.Button, self: *PaletteEditor) callconv(.c) void {
|
|
// Only the generated palette goes. Colors set by hand were set against
|
|
// what was underneath them and are still what their owner asked for, so
|
|
// dropping those too is `onReset`'s job and is labeled as such.
|
|
Settings.get().colors.of(theme.currentScheme()).tint = null;
|
|
self.applied();
|
|
}
|
|
|
|
fn onContrastChanged(scale: *gtk.Scale, self: *PaletteEditor) callconv(.c) void {
|
|
if (self.updating) return;
|
|
|
|
const changes = Settings.get().colors.of(theme.currentScheme());
|
|
|
|
// The slider is insensitive without a base, so this is unreachable in the
|
|
// normal way — but the property can still be set from a screen reader or a
|
|
// theme change landing mid-drag, and a contrast with nothing to spread is
|
|
// not worth a repaint.
|
|
if (changes.tint) |*tint| {
|
|
tint.contrast = @floatCast(scale.as(gtk.Range).getValue());
|
|
self.appliedLive();
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Small helpers
|
|
|
|
fn hint(text: [:0]const u8) *gtk.Widget {
|
|
const label = gtk.Label.new(text);
|
|
label.setXalign(0);
|
|
label.setWrap(1);
|
|
label.as(gtk.Widget).addCssClass("playpen-dialog-hint");
|
|
return label.as(gtk.Widget);
|
|
}
|
|
|
|
fn toRgba(color: palette.Rgb) gdk.RGBA {
|
|
return .{
|
|
.f_red = @as(f32, @floatFromInt(color.r)) / 255.0,
|
|
.f_green = @as(f32, @floatFromInt(color.g)) / 255.0,
|
|
.f_blue = @as(f32, @floatFromInt(color.b)) / 255.0,
|
|
.f_alpha = 1.0,
|
|
};
|
|
}
|
|
|
|
/// GTK works in floats and the palette works in bytes, so a color that came
|
|
/// out of the picker's own HSV wheel or its eyedropper has to be rounded to
|
|
/// something a hex triplet can hold. Rounding rather than truncating, so that a
|
|
/// color typed in as hex comes back as the same hex.
|
|
fn fromRgba(rgba: *const gdk.RGBA) palette.Rgb {
|
|
return .{
|
|
.r = channel(rgba.f_red),
|
|
.g = channel(rgba.f_green),
|
|
.b = channel(rgba.f_blue),
|
|
};
|
|
}
|
|
|
|
fn channel(value: f32) u8 {
|
|
return @intFromFloat(@round(std.math.clamp(value, 0.0, 1.0) * 255.0));
|
|
}
|