diff --git a/src/Window.zig b/src/Window.zig index 17b2de0..9f658f3 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -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); } diff --git a/src/palette.zig b/src/palette.zig index 55ceb8a..28d1d6d 100644 --- a/src/palette.zig +++ b/src/palette.zig @@ -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"), diff --git a/src/style.css b/src/style.css index 7005cf7..2c4dce9 100644 --- a/src/style.css +++ b/src/style.css @@ -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; } diff --git a/src/tint.zig b/src/tint.zig index bb75698..d837007 100644 --- a/src/tint.zig +++ b/src/tint.zig @@ -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); + } + } } }