Update tab styling.

This commit is contained in:
Greyson Parrelli
2026-08-27 11:02:32 -04:00
parent 0bf85b3b2a
commit 553e1f5424
4 changed files with 68 additions and 49 deletions
+8 -5
View File
@@ -1998,14 +1998,17 @@ fn applySidebarCollapsed(self: *Window, collapsed: bool) void {
for (self.tabs.items) |tab| self.applyRowCollapsed(tab);
}
/// One row's share of the collapse: everything but the emoji (or kind icon)
/// goes. The status dot goes too — the bar down the row's leading edge carries
/// the state at this width (with a wash behind it for the states that have one),
/// and it was designed to be the glanceable half of the pair anyway.
/// One row's share of the collapse: the title and the close button go, the
/// emoji (or kind icon) and the status dot stay.
///
/// The dot used to go as well, back when every state also drew a bar down the
/// row's leading edge and that bar could carry the state on its own. Only
/// "finished" draws one now, so at this width the dot is the whole signal for
/// the other three — and a collapsed sidebar is exactly where you are relying on
/// a glance rather than on reading anything.
fn applyRowCollapsed(self: *Window, tab: *Tab) void {
const shown: c_int = @intFromBool(!self.sidebar_collapsed);
tab.label.as(gtk.Widget).setVisible(shown);
tab.dot.as(gtk.Widget).setVisible(shown);
tab.close.as(gtk.Widget).setVisible(shown);
}
+14 -4
View File
@@ -184,6 +184,7 @@ pub const Key = enum {
accent_muted,
// ---- status ----
busy,
ok,
warn,
err,
@@ -226,7 +227,7 @@ pub const Key = enum {
.text, .text_dim, .text_faint => .text,
.row_hover, .row_selected => .rows,
.accent, .accent_strong, .accent_muted => .accents,
.ok, .warn, .err => .states,
.busy, .ok, .warn, .err => .states,
.term_bg, .term_fg, .term_cursor, .term_selection_bg, .term_selection_fg => .terminal,
else => .ansi,
};
@@ -289,6 +290,7 @@ pub const Key = enum {
.accent_strong => "Accent, strong",
.accent_muted => "Accent, muted",
.busy => "Working",
.ok => "Finished",
.warn => "Waiting on you",
.err => "Failed",
@@ -329,6 +331,7 @@ pub const Key = enum {
.text_faint => "Things that are labeled rather than read, like the title of a pane you aren't in",
.accent_strong => "Small marks that have to stay legible on a surface: icons, dots, a row's state bar",
.accent_muted => "Washes and the border of the focused pane",
.busy => "A pane that is still working. Its own color rather than the accent, so it stays distinct from \"finished\" under any accent",
.ok => "A pane that finished and hasn't been looked at",
.warn => "A pane waiting for you to answer something",
.err => "A pane that failed",
@@ -493,9 +496,15 @@ const dark: Table = .initDefault(unset, .{
.accent_strong = h("#6191f3"),
.accent_muted = h("#2f5296"),
// The three states that are news. Kept away from the accent on purpose: the
// accent means "here" and these mean "something happened", and a sidebar
// where those are the same color answers neither question.
// The states. Kept away from the accent on purpose: the accent means "here"
// and these mean "something happened", and a sidebar where those are the
// same color answers neither question.
//
// `busy` starts out where the strong accent does, because that is the blue
// this palette was built around — but it is a key of its own so that an
// accent moved somewhere else (a green one especially) cannot land it on top
// of `ok`, which is the one distinction the dots exist to make.
.busy = h("#6191f3"),
.ok = h("#3ecf8e"),
.warn = h("#f0b849"),
.err = h("#f2717b"),
@@ -534,6 +543,7 @@ const light: Table = .initDefault(unset, .{
.accent_strong = h("#1851b4"),
.accent_muted = h("#a9c4f7"),
.busy = h("#1851b4"),
.ok = h("#12805a"),
.warn = h("#8a5a00"),
.err = h("#c23b47"),
+34 -33
View File
@@ -191,54 +191,54 @@ button.playpen-header-button:hover,
/* State on the row itself, not just on its dot.
A bar down the leading edge, and behind the row a wash of the same color for
the two states that actually want you. The dot says what a row is doing once
you are reading it; these are what let you sweep a sidebar of a dozen tabs
and land on the one that wants you without reading any of them.
One bar and two washes, and that is the whole vocabulary. Finished gets the
bar down its leading edge; the two states that actually want you get a wash
of their own color behind the row. The dot says what a row is doing once you
are reading it; these are what let you sweep a sidebar of a dozen tabs and
land on the one you care about without reading any of them.
The bar is an inset shadow rather than a real border because a border takes
part in layout: 3px of it would shove every label sideways as a tab changed
state, and a sidebar that twitches is worse than one that is hard to read.
Only finished carries a bar. Every state used to have one, drawn as an inset
shadow, and a shadow is clipped to the row's 8px corners: what landed was a
short bar with a curl at each end, four rows deep in slightly different
colors. Read as ornament rather than as signal, so it is gone from everything
but the one state whose bar is a plain rectangle.
These come after the :selected rules deliberately. Equal specificity means
source order decides, and a state outranks "this is the tab you are on". */
/* No wash for working. It is the most common state by far, and a sidebar where
half the rows are tinted all afternoon teaches you to stop looking. */
.playpen-list > row.playpen-attn-busy {
box-shadow: inset 3px 0 0 @pp_accent_strong;
}
/* Working is the dot alone. It is the most common state by far, and a sidebar
where half the rows are marked all afternoon teaches you to stop looking. */
/* No wash for finished either, for a different reason: a tinted row is what
selection looks like in this list, so washing the finished ones left "the tab
you are on" and "the tab that just finished" saying the same thing. The dot
and a bar are enough — nothing here is waiting on you.
/* No wash for finished, for a different reason: a tinted row is what selection
looks like in this list, so washing the finished ones left "the tab you are
on" and "the tab that just finished" saying the same thing. The dot and the
bar are enough — nothing here is waiting on you.
The bar is a background image rather than the inset shadow the other states
use, because a shadow is clipped to the row's 8px corners and comes out
tapered at both ends. Kept short enough to clear those corners entirely, so
what lands is a plain rectangle. */
The bar is a background image rather than an inset shadow precisely because
it is not clipped to the corners, so it stays a rectangle at any height. Tall
enough now to read as a deliberate mark rather than a stray pixel, and still
short of the row's full height so it reads as pinned to the middle of the
row. */
.playpen-list > row.playpen-attn-done {
background-image: linear-gradient(@pp_ok, @pp_ok);
background-size: 3px 12px;
background-size: 3px 18px;
background-position: left center;
background-repeat: no-repeat;
}
.playpen-list > row.playpen-attn-input {
box-shadow: inset 3px 0 0 @pp_warn;
background-color: alpha(@pp_warn, 0.14);
}
.playpen-list > row.playpen-attn-failed {
box-shadow: inset 3px 0 0 @pp_err;
background-color: alpha(@pp_err, 0.14);
}
/* The current row keeps its own background: the wash and the selection would
otherwise blend into a color that reads as neither. The bar survives, which
is the part that carries the state. Done is absent because it no longer has a
wash to suppress. */
otherwise blend into a color that reads as neither, and the dot is left to
carry the state on its own — which on the row you are already looking at is
enough. Done is absent because it has no wash to suppress, only a bar, and a
bar and a selection sit together fine. */
.playpen-list > row:selected.playpen-attn-input,
.playpen-list > row:selected.playpen-attn-failed {
background-color: @pp_row_selected;
@@ -256,7 +256,6 @@ button.playpen-header-button:hover,
opacity: 0.65;
background-color: alpha(@pp_accent_strong, 0.18);
background-image: none;
box-shadow: inset 3px 0 0 @pp_accent_strong;
}
/* The copy of that row traveling with the pointer, which GTK draws into a
@@ -384,16 +383,18 @@ dnd.playpen-tab-drag {
opacity: 1;
}
/* Working: the same accent as an active pane's border, so "busy" reads as
ordinary activity rather than something gone wrong. */
/* Working: blue, and pulsing. Its own palette color rather than the accent —
the accent is whatever the theme is built around, and a green one put "still
working" in the same color as "finished", which is the one distinction this
whole indicator exists to make. The pulse carries the rest of it: a mark that
moves reads as something in progress before you have registered its color. */
.playpen-status-busy {
color: @pp_accent_strong;
color: @pp_busy;
animation: playpen-pulse 1.6s ease-in-out infinite;
}
/* Finished while you were elsewhere. Green rather than the accent so it is
distinct from still-working at a glance, which is the one distinction this
whole indicator exists to make. */
/* Finished while you were elsewhere. Green, and still — the dot that has
stopped moving is the one that has stopped working. */
.playpen-status-done {
color: @pp_ok;
}
+12 -7
View File
@@ -270,6 +270,7 @@ const dark_stops: Stops = .initDefault(inherited, .{
.accent_strong = accent(0.0742, 0.797),
.accent_muted = accent(-0.1450, 0.610),
.busy = named(0.6682, 0.1549, 263.2),
.ok = named(0.7624, 0.1544, 159.4),
.warn = named(0.8142, 0.1403, 81.3),
.err = named(0.7043, 0.1588, 17.3),
@@ -324,6 +325,7 @@ const light_stops: Stops = .initDefault(inherited, .{
.accent_strong = accent(-0.1040, 0.812),
.accent_muted = accent(0.2529, 0.377),
.busy = named(0.4613, 0.1680, 260.8),
.ok = named(0.5329, 0.1101, 163.1),
.warn = named(0.5078, 0.1080, 73.3),
.err = named(0.5530, 0.1708, 19.9),
@@ -527,12 +529,15 @@ test "a gray base makes a gray theme, and keeps the status colors apart" {
try testing.expect(oklab.fromRgb(derive(key, scheme, tint)).c < 0.01);
}
// The three states still have to be three states.
const ok = derive(.ok, scheme, tint);
const warn = derive(.warn, scheme, tint);
const err = derive(.err, scheme, tint);
try testing.expect(drift(ok, warn) > 20);
try testing.expect(drift(warn, err) > 20);
try testing.expect(drift(ok, err) > 20);
// The four states still have to be four states. `busy` against `ok` is
// the pair that matters most — a working tab and a finished one are what
// the dots are read for — but a gray theme is where any two of these
// would collapse if they were going to.
const states = [_]Key{ .busy, .ok, .warn, .err };
for (states, 0..) |a, i| {
for (states[i + 1 ..]) |b| {
try testing.expect(drift(derive(a, scheme, tint), derive(b, scheme, tint)) > 20);
}
}
}
}