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
+82 -3
View File
@@ -18,12 +18,18 @@ const Layouts = @import("Layouts.zig");
const OpenLayoutDialog = @import("OpenLayoutDialog.zig");
const Pane = @import("Pane.zig");
const SaveLayoutDialog = @import("SaveLayoutDialog.zig");
const SettingsDialog = @import("SettingsDialog.zig");
const Terminal = @import("Terminal.zig");
const View = @import("View.zig");
const appearance = @import("appearance.zig");
const Window = @This();
const sidebar_width = 220;
/// Total width of the sidebar column, margins and border included — GTK folds
/// both into a widget's size request. Widened when the sidebar became an inset
/// card: the margin, border and roomier row padding all come out of the label,
/// and at the old 220 a tab name truncated a good deal earlier than it used to.
const sidebar_width = 236;
alloc: std.mem.Allocator,
window: *adw.ApplicationWindow,
@@ -147,6 +153,7 @@ pub fn create(alloc: std.mem.Allocator, app: *adw.Application) !*Window {
header.as(gtk.Widget).addCssClass("flat");
const new_tab_button = gtk.Button.newFromIconName("tab-new-symbolic");
new_tab_button.as(gtk.Widget).addCssClass("playpen-header-button");
new_tab_button.as(gtk.Widget).setTooltipText("New tab (Ctrl+Shift+T)");
_ = gtk.Button.signals.clicked.connect(
new_tab_button,
@@ -155,17 +162,22 @@ pub fn create(alloc: std.mem.Allocator, app: *adw.Application) !*Window {
self,
.{},
);
header.packEnd(new_tab_button.as(gtk.Widget));
// Packed at the start, with the window controls left alone at the end. In
// a 220px header the two used to sit right up against the close button,
// which both wasted the empty half of the bar and put "new tab" a few
// pixels from "close window".
header.packStart(new_tab_button.as(gtk.Widget));
// Layouts sit behind their own button rather than replacing the plain
// new-tab one: opening an ordinary shell stays a single click.
const layout_button = gtk.MenuButton.new();
layout_button.setIconName("view-grid-symbolic");
layout_button.as(gtk.Widget).addCssClass("playpen-header-button");
layout_button.as(gtk.Widget).setTooltipText("Open a saved layout");
layout_button.setPopover(self.layout_popover);
self.layout_popover.as(gtk.Widget).addCssClass("playpen-layout-popover");
self.refreshLayoutMenu();
header.packEnd(layout_button.as(gtk.Widget));
header.packStart(layout_button.as(gtk.Widget));
sidebar.append(header.as(gtk.Widget));
self.list.setSelectionMode(.single);
@@ -185,6 +197,28 @@ pub fn create(alloc: std.mem.Allocator, app: *adw.Application) !*Window {
scroller.setChild(self.list.as(gtk.Widget));
sidebar.append(scroller.as(gtk.Widget));
// Settings sit at the foot of the sidebar rather than in its header. The
// header holds the two things you reach for constantly — a new tab and a
// saved layout — and a preferences button is the opposite of that: opened
// rarely, and never in a hurry. Below the tab list it stays out of the way
// of both, and it is where every other sidebar puts it.
const footer = gtk.Box.new(.horizontal, 0);
footer.as(gtk.Widget).addCssClass("playpen-sidebar-footer");
const settings_button = gtk.Button.newFromIconName("emblem-system-symbolic");
settings_button.as(gtk.Widget).addCssClass("flat");
settings_button.as(gtk.Widget).addCssClass("playpen-settings-button");
settings_button.as(gtk.Widget).setTooltipText("Settings (Ctrl+,)");
_ = gtk.Button.signals.clicked.connect(
settings_button,
*Window,
&onSettingsClicked,
self,
.{},
);
footer.append(settings_button.as(gtk.Widget));
sidebar.append(footer.as(gtk.Widget));
// ---- content -------------------------------------------------------
self.stack.as(gtk.Widget).setHexpand(1);
self.stack.as(gtk.Widget).setVexpand(1);
@@ -220,6 +254,8 @@ pub fn create(alloc: std.mem.Allocator, app: *adw.Application) !*Window {
.{},
);
appearance.onChanged(&onAppearanceChanged, self);
try self.newTab();
return self;
@@ -717,6 +753,40 @@ fn closeTab(self: *Window, tab: *Tab) void {
self.select(self.tabs.items[next]);
}
// -------------------------------------------------------------------------
// Settings
fn openSettings(self: *Window) void {
SettingsDialog.present(self.alloc, self.window.as(gtk.Window)) catch |err| {
std.log.err("failed to open settings: {s}", .{@errorName(err)});
};
}
fn onSettingsClicked(_: *gtk.Button, self: *Window) callconv(.c) void {
self.openSettings();
}
/// The colour scheme changed. Everything styled by CSS restyles itself; the
/// terminal grids do not, because Cairo draws them from `theme.zig` and GTK
/// has no idea that widget's contents depend on the palette at all. Without
/// this, switching to light leaves every terminal a dark rectangle until
/// something else happens to dirty it.
///
/// Sessions already open are re-palletted rather than left on the one they
/// started in: a shell you have had running all day is exactly the one you are
/// looking at when you switch, and leaving it in the old scheme's colours
/// would make the setting look like it only applies to new tabs.
fn onAppearanceChanged(ctx: ?*anyopaque) void {
const self: *Window = @ptrCast(@alignCast(ctx.?));
for (self.tabs.items) |tab| {
for (tab.view.panes.items) |pane| {
const terminal = pane.terminal() orelse continue;
terminal.session.refreshPalette();
terminal.area.as(gtk.Widget).queueDraw();
}
}
}
// -------------------------------------------------------------------------
// Signal handlers
@@ -813,6 +883,10 @@ fn onDestroy(_: *adw.ApplicationWindow, self: *Window) callconv(.c) void {
if (self.closing) return;
self.closing = true;
// Before anything is freed: a scheme change arriving mid-teardown would
// otherwise walk a tab list we are about to destroy.
appearance.clearOnChanged();
// Each terminal owns a session, which owns a PTY and its child process.
// Dropping them here reaps the children rather than orphaning them.
for (self.tabs.items) |tab| {
@@ -943,8 +1017,13 @@ fn onShortcut(
}
// Ctrl+PageUp/PageDown cycles tabs, matching most tabbed terminals.
// Ctrl+comma opens settings, which is the convention nearly everywhere.
if (ctrl and !shift) {
switch (keyval) {
gdk.KEY_comma => {
self.openSettings();
return 1;
},
gdk.KEY_Page_Up => {
self.cycle(-1);
return 1;