show names on tabs in tabbed layout
This commit is contained in:
@@ -189,9 +189,12 @@ order — the first master slot — and takes focus, as in dwm. A window spawned
|
||||
onto a tag nobody is viewing leaves both the order and focus alone.
|
||||
|
||||
**tabbed** — same geometry as monocle, minus a strip at the top where att_wm
|
||||
draws one solid colour block per window, the focused one highlighted. The blocks
|
||||
are clickable. Titles are *not* drawn — that would mean a font stack — but the
|
||||
window list is published over IPC, so a bar can draw a textual tab strip
|
||||
draws one tab per window, the focused one highlighted. Each tab is clickable and
|
||||
carries the window's title, elided with an ellipsis when the tab is too narrow.
|
||||
Titles are drawn in a 5x8 bitmap font built into the binary (`src/font.zig`) —
|
||||
a font stack for one strip of pixels would be a poor trade — scaled by whole
|
||||
pixels to suit `tabbar_height`, or by `tab_font_scale` if you would rather say.
|
||||
The same window list is published over IPC, so a bar can draw its own tab strip
|
||||
alongside (the bundled quickshell config does). Set `tabbar_height = 0` to drop
|
||||
the built-in strip entirely and let the bar own it.
|
||||
|
||||
@@ -217,9 +220,10 @@ zig build -Dconfig=/path/to/my-config.zig
|
||||
att_wm.override { configFile = ./my-config.zig; }
|
||||
```
|
||||
|
||||
`config.zig` covers border width and colours, gaps, tab bar height and colours,
|
||||
the default layout, `nmaster` / `mfact`, tag names, focus-follows-mouse, key and
|
||||
pointer bindings, autostart commands, input devices, and window rules:
|
||||
`config.zig` covers border width and colours, gaps, tab bar height, colours and
|
||||
text size, the default layout, `nmaster` / `mfact`, tag names,
|
||||
focus-follows-mouse, key and pointer bindings, autostart commands, input
|
||||
devices, and window rules:
|
||||
|
||||
```zig
|
||||
pub const rules = [_]Rule{
|
||||
@@ -487,7 +491,8 @@ river's identifier is shared between them.
|
||||
| `src/input.zig` | Input configuration types — no Wayland, unit tested |
|
||||
| `src/config.zig` | Compile-time configuration |
|
||||
| `src/ipc.zig` | Socket server and JSON encoding |
|
||||
| `src/shm.zig` | memfd buffers for the tab bar |
|
||||
| `src/shm.zig` | memfd buffers for the tab bar, and drawing into them |
|
||||
| `src/font.zig` | The built-in 5x8 bitmap font — no Wayland, unit tested |
|
||||
| `src/sys.zig` | Thin Linux syscall wrappers |
|
||||
|
||||
The single most important invariant: **river only permits window management
|
||||
|
||||
+51
-3
@@ -21,6 +21,7 @@ const Wm = @import("Wm.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const shm = @import("shm.zig");
|
||||
const color = @import("color.zig");
|
||||
const font = @import("font.zig");
|
||||
const layout = @import("layout.zig");
|
||||
const Box = layout.Box;
|
||||
|
||||
@@ -157,9 +158,10 @@ fn onLayerEvent(_: *river.LayerShellOutputV1, event: river.LayerShellOutputV1.Ev
|
||||
}
|
||||
}
|
||||
|
||||
/// The strip of solid colour blocks drawn above the windows in the tabbed
|
||||
/// layout. One block per window, the focused one highlighted; titles are not
|
||||
/// drawn here but are published over IPC for bars that want to render them.
|
||||
/// The strip of tabs drawn above the windows in the tabbed layout. One tab per
|
||||
/// window, labelled with its title and the focused one highlighted. The same
|
||||
/// titles are published over IPC, for bars that would rather draw the tabs
|
||||
/// themselves with a real font.
|
||||
pub const TabBar = struct {
|
||||
wm: *Wm,
|
||||
|
||||
@@ -240,6 +242,7 @@ pub const TabBar = struct {
|
||||
const sep = color.toArgb8888(config.tab_separator);
|
||||
buffer.fill(local, sep);
|
||||
|
||||
const scale = textScale(box.height);
|
||||
for (self.rects.items, windows) |rect, win| {
|
||||
const is_focused = focused != null and focused.? == win;
|
||||
const argb = color.toArgb8888(if (is_focused) config.tab_focused else config.tab_normal);
|
||||
@@ -252,6 +255,9 @@ pub const TabBar = struct {
|
||||
.height = rect.height,
|
||||
};
|
||||
buffer.fill(inner, argb);
|
||||
|
||||
const ink = color.toArgb8888(if (is_focused) config.tab_text_focused else config.tab_text_normal);
|
||||
drawLabel(buffer, inner, label(win), scale, ink);
|
||||
}
|
||||
|
||||
// Translate the hit rectangles into global coordinates for click
|
||||
@@ -286,6 +292,48 @@ pub const TabBar = struct {
|
||||
self.windows.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
/// What to write on a window's tab: its title, or its app id while it has
|
||||
/// not set one, so that a tab is never blank.
|
||||
fn label(win: *const Window) []const u8 {
|
||||
if (win.title) |t| {
|
||||
if (t.len > 0) return t;
|
||||
}
|
||||
if (win.app_id) |a| {
|
||||
if (a.len > 0) return a;
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
/// Whole-pixel scale for the font: the largest that leaves a little air
|
||||
/// above and below in a bar this tall. `config.tab_font_scale` overrides
|
||||
/// it for anyone wanting big letters in a thin bar, or the reverse.
|
||||
fn textScale(height: i32) i32 {
|
||||
if (config.tab_font_scale > 0) return config.tab_font_scale;
|
||||
var s: i32 = 1;
|
||||
while (s < 4 and font.cell_height * (s + 1) + 4 <= height) s += 1;
|
||||
return s;
|
||||
}
|
||||
|
||||
/// Centre a title in one tab, eliding it with an ellipsis when the tab is
|
||||
/// too narrow to hold it.
|
||||
fn drawLabel(buffer: *shm.Buffer, tab: Box, text: []const u8, scale: i32, argb: u32) void {
|
||||
const pad = 2 * scale;
|
||||
const avail = tab.width - 2 * pad;
|
||||
if (avail <= 0) return;
|
||||
|
||||
const f = font.fit(text, font.cellsForWidth(avail, scale));
|
||||
const cells = f.cells + @intFromBool(f.elided);
|
||||
if (cells == 0) return;
|
||||
|
||||
const x = tab.x + @divFloor(tab.width - font.textWidth(cells, scale), 2);
|
||||
// Centre the part above the baseline; the descender row may hang into
|
||||
// the padding, which is what it is for.
|
||||
const y = tab.y + @divFloor(tab.height - (font.cell_height - 1) * scale, 2);
|
||||
|
||||
const pen = buffer.drawText(text[0..f.end], x, y, scale, argb, tab);
|
||||
if (f.elided) buffer.drawGlyph(font.ellipsis, pen, y, scale, argb, tab);
|
||||
}
|
||||
|
||||
/// Which window's tab covers this global coordinate, if any.
|
||||
pub fn windowAt(self: *const TabBar, x: i32, y: i32) ?*Window {
|
||||
if (!self.mapped) return null;
|
||||
|
||||
@@ -137,6 +137,15 @@ pub fn applyRules(self: *Window) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this window currently has a tab drawn for it, i.e. it is tiled and
|
||||
/// visible on an output that is showing the tabbed layout.
|
||||
fn onTabBar(self: *Window) bool {
|
||||
if (config.tabbar_height <= 0) return false;
|
||||
if (!self.visible or self.floating or self.fullscreen or self.closed) return false;
|
||||
const out = self.output orelse return false;
|
||||
return out.state().layout == .tabbed;
|
||||
}
|
||||
|
||||
fn setString(self: *Window, field: *?[]u8, value: ?[*:0]const u8) void {
|
||||
const gpa = self.wm.gpa;
|
||||
if (field.*) |old| gpa.free(old);
|
||||
@@ -190,6 +199,10 @@ fn onEvent(_: *river.WindowV1, event: river.WindowV1.Event, self: *Window) void
|
||||
self.setString(&self.title, ev.title);
|
||||
self.applyRules();
|
||||
self.wm.ipcDirty();
|
||||
// The tab bar has this title painted into it, so a window that is
|
||||
// wearing a tab needs the strip redrawn. Terminals retitle
|
||||
// themselves constantly, so ask only when it will show.
|
||||
if (self.onTabBar()) self.wm.needsManage();
|
||||
},
|
||||
|
||||
.identifier => |ev| {
|
||||
|
||||
@@ -41,6 +41,14 @@ pub const tab_normal: u32 = 0x2c2c2cff;
|
||||
/// Drawn as a 1px line between adjacent tabs.
|
||||
pub const tab_separator: u32 = 0x1a1a1aff;
|
||||
|
||||
/// Window titles are drawn on the tabs in these colours.
|
||||
pub const tab_text_focused: u32 = 0xffffffff;
|
||||
pub const tab_text_normal: u32 = 0xbbbbbbff;
|
||||
/// Whole-pixel scale for the 5x8 built-in font the titles are drawn in: 2
|
||||
/// gives 10x16 letters. Zero picks the largest scale `tabbar_height` has room
|
||||
/// for, which is what you want after changing the bar height.
|
||||
pub const tab_font_scale: i32 = 0;
|
||||
|
||||
// ─── Layout ──────────────────────────────────────────────────────────────────
|
||||
|
||||
pub const default_layout = action.Layout.master;
|
||||
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
//! A 5x8 bitmap font, and the text measuring that goes with it.
|
||||
//!
|
||||
//! The tab bar has to label its tabs, and a window manager that draws exactly
|
||||
//! one strip of pixels has no business linking a font stack for it. So the
|
||||
//! glyphs live here as data: printable ASCII plus an ellipsis for elided
|
||||
//! titles, drawn as bit rows and expanded by whole pixels when scaled.
|
||||
//!
|
||||
//! Anything else — every non-ASCII codepoint a title may hold — renders as the
|
||||
//! fallback box, which is at least honest about being a character we cannot
|
||||
//! draw.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
pub const cell_width = 5;
|
||||
pub const cell_height = 8;
|
||||
/// Columns from one cell to the next: the glyph plus a one column gap.
|
||||
pub const advance = cell_width + 1;
|
||||
|
||||
/// One glyph: `cell_height` rows, bit `cell_width - 1` the leftmost column.
|
||||
pub const Glyph = [cell_height]u8;
|
||||
|
||||
pub const first_char = 0x20;
|
||||
pub const last_char = 0x7e;
|
||||
|
||||
pub const ellipsis: u21 = '…';
|
||||
|
||||
/// Rows for a codepoint. Control characters come back blank rather than as a
|
||||
/// box, so a stray newline in a title does not turn into visible noise.
|
||||
pub fn glyph(cp: u21) *const Glyph {
|
||||
if (cp < first_char or cp == 0x7f) return &glyphs[0];
|
||||
if (cp <= last_char) return &glyphs[cp - first_char];
|
||||
if (cp == ellipsis) return &ellipsis_glyph;
|
||||
return &fallback_glyph;
|
||||
}
|
||||
|
||||
/// Width in pixels of `cells` characters, without the trailing gap.
|
||||
pub fn textWidth(cells: usize, scale: i32) i32 {
|
||||
if (cells == 0) return 0;
|
||||
const n: i32 = @intCast(cells);
|
||||
return (n * advance - 1) * scale;
|
||||
}
|
||||
|
||||
/// How many characters fit in a strip that wide.
|
||||
pub fn cellsForWidth(width: i32, scale: i32) usize {
|
||||
if (width <= 0 or scale <= 0) return 0;
|
||||
// The last cell needs no trailing gap, so lend it one.
|
||||
const cells = @divFloor(width + scale, advance * scale);
|
||||
return if (cells <= 0) 0 else @intCast(cells);
|
||||
}
|
||||
|
||||
pub const Fit = struct {
|
||||
/// Bytes of the string to draw.
|
||||
end: usize,
|
||||
/// Characters those bytes come to, an ellipsis not counted.
|
||||
cells: usize,
|
||||
/// The string was too long; draw an ellipsis after `end`.
|
||||
elided: bool,
|
||||
};
|
||||
|
||||
/// Fit a string into `max_cells` character cells, giving up the last cell to
|
||||
/// an ellipsis when the string does not fit whole.
|
||||
pub fn fit(text: []const u8, max_cells: usize) Fit {
|
||||
if (max_cells == 0) return .{ .end = 0, .cells = 0, .elided = false };
|
||||
|
||||
var it: Iterator = .{ .bytes = text };
|
||||
var cells: usize = 0;
|
||||
// Where the string is cut if it turns out not to fit: one cell short, so
|
||||
// the ellipsis has somewhere to go.
|
||||
var cut: usize = 0;
|
||||
|
||||
while (it.next()) |_| {
|
||||
cells += 1;
|
||||
if (cells + 1 == max_cells) cut = it.i;
|
||||
// One character past the end is what proves it does not fit.
|
||||
if (cells > max_cells) {
|
||||
return .{ .end = cut, .cells = max_cells - 1, .elided = true };
|
||||
}
|
||||
}
|
||||
return .{ .end = text.len, .cells = cells, .elided = false };
|
||||
}
|
||||
|
||||
/// UTF-8 iteration that never fails: anything malformed comes back as the
|
||||
/// replacement character, which draws as the fallback box. Titles come from
|
||||
/// arbitrary clients, so refusing to render one is not an option.
|
||||
pub const Iterator = struct {
|
||||
bytes: []const u8,
|
||||
i: usize = 0,
|
||||
|
||||
const replacement: u21 = 0xfffd;
|
||||
|
||||
pub fn next(self: *Iterator) ?u21 {
|
||||
if (self.i >= self.bytes.len) return null;
|
||||
|
||||
const len = std.unicode.utf8ByteSequenceLength(self.bytes[self.i]) catch {
|
||||
self.i += 1;
|
||||
return replacement;
|
||||
};
|
||||
if (self.i + len > self.bytes.len) {
|
||||
self.i = self.bytes.len;
|
||||
return replacement;
|
||||
}
|
||||
const cp = std.unicode.utf8Decode(self.bytes[self.i..][0..len]) catch {
|
||||
self.i += len;
|
||||
return replacement;
|
||||
};
|
||||
self.i += len;
|
||||
return cp;
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Glyph data ──────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Written as pictures rather than hex so a wrong pixel is visible in review.
|
||||
// Rows 0..6 hold the glyph, with the baseline under row 6; row 7 is there for
|
||||
// the descenders of g, j, p, q and y.
|
||||
|
||||
fn parse(comptime picture: [cell_height][]const u8) Glyph {
|
||||
var rows: Glyph = @splat(0);
|
||||
for (picture, 0..) |line, y| {
|
||||
if (line.len != cell_width) @compileError("glyph row must be " ++
|
||||
std.fmt.comptimePrint("{d}", .{cell_width}) ++ " columns wide");
|
||||
for (line, 0..) |ch, x| {
|
||||
if (ch == '#') rows[y] |= @as(u8, 1) << @intCast(cell_width - 1 - x);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
const fallback_glyph = parse(.{ ".....", "#####", "#...#", "#...#", "#...#", "#...#", "#####", "....." });
|
||||
const ellipsis_glyph = parse(.{ ".....", ".....", ".....", ".....", ".....", ".....", "#.#.#", "....." });
|
||||
|
||||
const glyphs = blk: {
|
||||
// Ninety-five glyphs of forty pixels each is a lot of comptime looping.
|
||||
@setEvalBranchQuota(20_000);
|
||||
var table: [last_char - first_char + 1]Glyph = undefined;
|
||||
for (art, 0..) |a, i| table[i] = parse(a);
|
||||
break :blk table;
|
||||
};
|
||||
|
||||
const art = [_][cell_height][]const u8{
|
||||
.{ ".....", ".....", ".....", ".....", ".....", ".....", ".....", "....." }, // space
|
||||
.{ "..#..", "..#..", "..#..", "..#..", "..#..", ".....", "..#..", "....." }, // !
|
||||
.{ ".#.#.", ".#.#.", ".#.#.", ".....", ".....", ".....", ".....", "....." }, // "
|
||||
.{ ".#.#.", ".#.#.", "#####", ".#.#.", "#####", ".#.#.", ".#.#.", "....." }, // #
|
||||
.{ "..#..", ".####", "#.#..", ".###.", "..#.#", "####.", "..#..", "....." }, // $
|
||||
.{ "##...", "##..#", "...#.", "..#..", ".#...", "#..##", "...##", "....." }, // %
|
||||
.{ ".##..", "#..#.", "#.#..", ".#...", "#.#.#", "#..#.", ".##.#", "....." }, // &
|
||||
.{ "..#..", "..#..", "..#..", ".....", ".....", ".....", ".....", "....." }, // '
|
||||
.{ "...#.", "..#..", ".#...", ".#...", ".#...", "..#..", "...#.", "....." }, // (
|
||||
.{ ".#...", "..#..", "...#.", "...#.", "...#.", "..#..", ".#...", "....." }, // )
|
||||
.{ ".....", "#.#.#", ".###.", "#####", ".###.", "#.#.#", ".....", "....." }, // *
|
||||
.{ ".....", "..#..", "..#..", "#####", "..#..", "..#..", ".....", "....." }, // +
|
||||
.{ ".....", ".....", ".....", ".....", ".....", "..##.", "..##.", ".#..." }, // ,
|
||||
.{ ".....", ".....", ".....", "#####", ".....", ".....", ".....", "....." }, // -
|
||||
.{ ".....", ".....", ".....", ".....", ".....", "..##.", "..##.", "....." }, // .
|
||||
.{ "....#", "...#.", "...#.", "..#..", ".#...", ".#...", "#....", "....." }, // /
|
||||
.{ ".###.", "#...#", "#..##", "#.#.#", "##..#", "#...#", ".###.", "....." }, // 0
|
||||
.{ "..#..", ".##..", "..#..", "..#..", "..#..", "..#..", ".###.", "....." }, // 1
|
||||
.{ ".###.", "#...#", "....#", "...#.", "..#..", ".#...", "#####", "....." }, // 2
|
||||
.{ "#####", "...#.", "..#..", "...#.", "....#", "#...#", ".###.", "....." }, // 3
|
||||
.{ "...#.", "..##.", ".#.#.", "#..#.", "#####", "...#.", "...#.", "....." }, // 4
|
||||
.{ "#####", "#....", "####.", "....#", "....#", "#...#", ".###.", "....." }, // 5
|
||||
.{ "..##.", ".#...", "#....", "####.", "#...#", "#...#", ".###.", "....." }, // 6
|
||||
.{ "#####", "....#", "...#.", "..#..", ".#...", ".#...", ".#...", "....." }, // 7
|
||||
.{ ".###.", "#...#", "#...#", ".###.", "#...#", "#...#", ".###.", "....." }, // 8
|
||||
.{ ".###.", "#...#", "#...#", ".####", "....#", "...#.", ".##..", "....." }, // 9
|
||||
.{ ".....", ".....", "..##.", "..##.", ".....", "..##.", "..##.", "....." }, // :
|
||||
.{ ".....", ".....", "..##.", "..##.", ".....", "..##.", "..##.", ".#..." }, // ;
|
||||
.{ ".....", "...#.", "..#..", ".#...", "..#..", "...#.", ".....", "....." }, // <
|
||||
.{ ".....", ".....", "#####", ".....", "#####", ".....", ".....", "....." }, // =
|
||||
.{ ".....", ".#...", "..#..", "...#.", "..#..", ".#...", ".....", "....." }, // >
|
||||
.{ ".###.", "#...#", "....#", "...#.", "..#..", ".....", "..#..", "....." }, // ?
|
||||
.{ ".###.", "#...#", "#.###", "#.#.#", "#.###", "#....", ".###.", "....." }, // @
|
||||
.{ ".###.", "#...#", "#...#", "#####", "#...#", "#...#", "#...#", "....." }, // A
|
||||
.{ "####.", "#...#", "#...#", "####.", "#...#", "#...#", "####.", "....." }, // B
|
||||
.{ ".###.", "#...#", "#....", "#....", "#....", "#...#", ".###.", "....." }, // C
|
||||
.{ "###..", "#..#.", "#...#", "#...#", "#...#", "#..#.", "###..", "....." }, // D
|
||||
.{ "#####", "#....", "#....", "####.", "#....", "#....", "#####", "....." }, // E
|
||||
.{ "#####", "#....", "#....", "####.", "#....", "#....", "#....", "....." }, // F
|
||||
.{ ".###.", "#...#", "#....", "#.###", "#...#", "#...#", ".####", "....." }, // G
|
||||
.{ "#...#", "#...#", "#...#", "#####", "#...#", "#...#", "#...#", "....." }, // H
|
||||
.{ ".###.", "..#..", "..#..", "..#..", "..#..", "..#..", ".###.", "....." }, // I
|
||||
.{ "..###", "...#.", "...#.", "...#.", "...#.", "#..#.", ".##..", "....." }, // J
|
||||
.{ "#...#", "#..#.", "#.#..", "##...", "#.#..", "#..#.", "#...#", "....." }, // K
|
||||
.{ "#....", "#....", "#....", "#....", "#....", "#....", "#####", "....." }, // L
|
||||
.{ "#...#", "##.##", "#.#.#", "#.#.#", "#...#", "#...#", "#...#", "....." }, // M
|
||||
.{ "#...#", "#...#", "##..#", "#.#.#", "#..##", "#...#", "#...#", "....." }, // N
|
||||
.{ ".###.", "#...#", "#...#", "#...#", "#...#", "#...#", ".###.", "....." }, // O
|
||||
.{ "####.", "#...#", "#...#", "####.", "#....", "#....", "#....", "....." }, // P
|
||||
.{ ".###.", "#...#", "#...#", "#...#", "#.#.#", "#..#.", ".##.#", "....." }, // Q
|
||||
.{ "####.", "#...#", "#...#", "####.", "#.#..", "#..#.", "#...#", "....." }, // R
|
||||
.{ ".####", "#....", "#....", ".###.", "....#", "....#", "####.", "....." }, // S
|
||||
.{ "#####", "..#..", "..#..", "..#..", "..#..", "..#..", "..#..", "....." }, // T
|
||||
.{ "#...#", "#...#", "#...#", "#...#", "#...#", "#...#", ".###.", "....." }, // U
|
||||
.{ "#...#", "#...#", "#...#", "#...#", "#...#", ".#.#.", "..#..", "....." }, // V
|
||||
.{ "#...#", "#...#", "#...#", "#.#.#", "#.#.#", "##.##", "#...#", "....." }, // W
|
||||
.{ "#...#", "#...#", ".#.#.", "..#..", ".#.#.", "#...#", "#...#", "....." }, // X
|
||||
.{ "#...#", "#...#", ".#.#.", "..#..", "..#..", "..#..", "..#..", "....." }, // Y
|
||||
.{ "#####", "....#", "...#.", "..#..", ".#...", "#....", "#####", "....." }, // Z
|
||||
.{ ".###.", ".#...", ".#...", ".#...", ".#...", ".#...", ".###.", "....." }, // [
|
||||
.{ "#....", "#....", ".#...", "..#..", "...#.", "....#", "....#", "....." }, // backslash
|
||||
.{ ".###.", "...#.", "...#.", "...#.", "...#.", "...#.", ".###.", "....." }, // ]
|
||||
.{ "..#..", ".#.#.", "#...#", ".....", ".....", ".....", ".....", "....." }, // ^
|
||||
.{ ".....", ".....", ".....", ".....", ".....", ".....", ".....", "#####" }, // _
|
||||
.{ ".#...", "..#..", ".....", ".....", ".....", ".....", ".....", "....." }, // `
|
||||
.{ ".....", ".....", ".###.", "....#", ".####", "#...#", ".####", "....." }, // a
|
||||
.{ "#....", "#....", "####.", "#...#", "#...#", "#...#", "####.", "....." }, // b
|
||||
.{ ".....", ".....", ".###.", "#...#", "#....", "#...#", ".###.", "....." }, // c
|
||||
.{ "....#", "....#", ".####", "#...#", "#...#", "#...#", ".####", "....." }, // d
|
||||
.{ ".....", ".....", ".###.", "#...#", "#####", "#....", ".###.", "....." }, // e
|
||||
.{ "..##.", ".#..#", ".#...", "####.", ".#...", ".#...", ".#...", "....." }, // f
|
||||
.{ ".....", ".....", ".####", "#...#", "#...#", ".####", "....#", ".###." }, // g
|
||||
.{ "#....", "#....", "####.", "#...#", "#...#", "#...#", "#...#", "....." }, // h
|
||||
.{ "..#..", ".....", ".##..", "..#..", "..#..", "..#..", ".###.", "....." }, // i
|
||||
.{ "...#.", ".....", "..##.", "...#.", "...#.", "...#.", "#..#.", ".##.." }, // j
|
||||
.{ "#....", "#....", "#..#.", "#.#..", "##...", "#.#..", "#..#.", "....." }, // k
|
||||
.{ ".##..", "..#..", "..#..", "..#..", "..#..", "..#..", ".###.", "....." }, // l
|
||||
.{ ".....", ".....", "##.#.", "#.#.#", "#.#.#", "#...#", "#...#", "....." }, // m
|
||||
.{ ".....", ".....", "####.", "#...#", "#...#", "#...#", "#...#", "....." }, // n
|
||||
.{ ".....", ".....", ".###.", "#...#", "#...#", "#...#", ".###.", "....." }, // o
|
||||
.{ ".....", ".....", "####.", "#...#", "#...#", "####.", "#....", "#...." }, // p
|
||||
.{ ".....", ".....", ".####", "#...#", "#...#", ".####", "....#", "....#" }, // q
|
||||
.{ ".....", ".....", "#.##.", "##..#", "#....", "#....", "#....", "....." }, // r
|
||||
.{ ".....", ".....", ".####", "#....", ".###.", "....#", "####.", "....." }, // s
|
||||
.{ ".#...", ".#...", "####.", ".#...", ".#...", ".#..#", "..##.", "....." }, // t
|
||||
.{ ".....", ".....", "#...#", "#...#", "#...#", "#...#", ".####", "....." }, // u
|
||||
.{ ".....", ".....", "#...#", "#...#", "#...#", ".#.#.", "..#..", "....." }, // v
|
||||
.{ ".....", ".....", "#...#", "#...#", "#.#.#", "#.#.#", ".#.#.", "....." }, // w
|
||||
.{ ".....", ".....", "#...#", ".#.#.", "..#..", ".#.#.", "#...#", "....." }, // x
|
||||
.{ ".....", ".....", "#...#", "#...#", "#...#", ".####", "....#", ".###." }, // y
|
||||
.{ ".....", ".....", "#####", "...#.", "..#..", ".#...", "#####", "....." }, // z
|
||||
.{ "...##", "..#..", "..#..", ".#...", "..#..", "..#..", "...##", "....." }, // {
|
||||
.{ "..#..", "..#..", "..#..", "..#..", "..#..", "..#..", "..#..", "....." }, // |
|
||||
.{ "##...", "..#..", "..#..", "...#.", "..#..", "..#..", "##...", "....." }, // }
|
||||
.{ ".....", ".....", ".##..", "#..##", ".....", ".....", ".....", "....." }, // ~
|
||||
};
|
||||
|
||||
comptime {
|
||||
std.debug.assert(art.len == last_char - first_char + 1);
|
||||
}
|
||||
+48
-2
@@ -1,8 +1,8 @@
|
||||
//! Minimal wl_shm buffer pool for the tab bar.
|
||||
//!
|
||||
//! The tab bar is the only thing att_wm draws itself, and it draws nothing but
|
||||
//! solid rectangles, so this deliberately stops at "memfd, mmap, fill" rather
|
||||
//! than pulling in pixman or a font stack.
|
||||
//! solid rectangles and text in the bitmap font from `font.zig`, so this
|
||||
//! deliberately stops at "memfd, mmap, fill" rather than pulling in pixman.
|
||||
|
||||
const std = @import("std");
|
||||
const posix = std.posix;
|
||||
@@ -12,6 +12,7 @@ const sys = @import("sys.zig");
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.client.wl;
|
||||
|
||||
const font = @import("font.zig");
|
||||
const layout = @import("layout.zig");
|
||||
const Box = layout.Box;
|
||||
|
||||
@@ -58,6 +59,51 @@ pub const Buffer = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill a rectangle, clipped to `clip` as well as to the buffer.
|
||||
pub fn fillClipped(self: *Buffer, rect: Box, clip: Box, argb: u32) void {
|
||||
const x0 = @max(rect.x, clip.x);
|
||||
const y0 = @max(rect.y, clip.y);
|
||||
const x1 = @min(rect.x + rect.width, clip.x + clip.width);
|
||||
const y1 = @min(rect.y + rect.height, clip.y + clip.height);
|
||||
if (x1 <= x0 or y1 <= y0) return;
|
||||
self.fill(.{ .x = x0, .y = y0, .width = x1 - x0, .height = y1 - y0 }, argb);
|
||||
}
|
||||
|
||||
/// Draw text with the top left of its first cell at (x, y), clipped to
|
||||
/// `clip`, and return the x the next cell would start at. The font has no
|
||||
/// antialiasing, so lit pixels are simply written: nothing to blend.
|
||||
pub fn drawText(self: *Buffer, text: []const u8, x: i32, y: i32, scale: i32, argb: u32, clip: Box) i32 {
|
||||
var pen = x;
|
||||
var it: font.Iterator = .{ .bytes = text };
|
||||
while (it.next()) |cp| {
|
||||
// Stop as soon as the pen leaves the clip rather than walking the
|
||||
// rest of a long title a pixel at a time.
|
||||
if (pen >= clip.x + clip.width) break;
|
||||
self.drawGlyph(cp, pen, y, scale, argb, clip);
|
||||
pen += font.advance * scale;
|
||||
}
|
||||
return pen;
|
||||
}
|
||||
|
||||
pub fn drawGlyph(self: *Buffer, cp: u21, x: i32, y: i32, scale: i32, argb: u32, clip: Box) void {
|
||||
const rows = font.glyph(cp);
|
||||
for (rows, 0..) |bits, row| {
|
||||
if (bits == 0) continue;
|
||||
const py = y + @as(i32, @intCast(row)) * scale;
|
||||
var col: u3 = 0;
|
||||
while (col < font.cell_width) : (col += 1) {
|
||||
const mask = @as(u8, 1) << @intCast(font.cell_width - 1 - col);
|
||||
if (bits & mask == 0) continue;
|
||||
self.fillClipped(.{
|
||||
.x = x + @as(i32, col) * scale,
|
||||
.y = py,
|
||||
.width = scale,
|
||||
.height = scale,
|
||||
}, clip, argb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deinit(self: *Buffer, gpa: Allocator) void {
|
||||
self.wl_buffer.destroy();
|
||||
posix.munmap(self.data);
|
||||
|
||||
@@ -3,6 +3,7 @@ const testing = std.testing;
|
||||
|
||||
const act = @import("action");
|
||||
const input = @import("input");
|
||||
const font = @import("font.zig");
|
||||
const layout = @import("layout.zig");
|
||||
const Box = layout.Box;
|
||||
|
||||
@@ -422,3 +423,87 @@ test "map_to_output merges like any other setting" {
|
||||
const moved = merged.merge(.{ .map_to_output = "DP-2" });
|
||||
try testing.expectEqualStrings("DP-2", moved.map_to_output.?);
|
||||
}
|
||||
|
||||
// ─── tab titles ──────────────────────────────────────────────────────────────
|
||||
|
||||
test "a title that fits is drawn whole" {
|
||||
const f = font.fit("foot", 10);
|
||||
try testing.expectEqual(@as(usize, 4), f.end);
|
||||
try testing.expectEqual(@as(usize, 4), f.cells);
|
||||
try testing.expect(!f.elided);
|
||||
|
||||
// Exactly full is still whole: the ellipsis costs a cell, so only spend it
|
||||
// when something is actually cut.
|
||||
const exact = font.fit("foot", 4);
|
||||
try testing.expectEqual(@as(usize, 4), exact.cells);
|
||||
try testing.expect(!exact.elided);
|
||||
}
|
||||
|
||||
test "a title too long for the tab loses its last cell to the ellipsis" {
|
||||
const f = font.fit("firefox", 4);
|
||||
try testing.expectEqualStrings("fir", "firefox"[0..f.end]);
|
||||
try testing.expectEqual(@as(usize, 3), f.cells);
|
||||
try testing.expect(f.elided);
|
||||
|
||||
// One cell of room leaves nothing but the ellipsis, and no room at all
|
||||
// draws nothing rather than indexing past the end.
|
||||
const one = font.fit("firefox", 1);
|
||||
try testing.expectEqual(@as(usize, 0), one.end);
|
||||
try testing.expect(one.elided);
|
||||
|
||||
const none = font.fit("firefox", 0);
|
||||
try testing.expectEqual(@as(usize, 0), none.end);
|
||||
try testing.expect(!none.elided);
|
||||
}
|
||||
|
||||
test "titles are measured in characters, not bytes" {
|
||||
// Three codepoints, six bytes: a tab four cells wide holds them all.
|
||||
const f = font.fit("ünïx", 4);
|
||||
try testing.expectEqual(@as(usize, 4), f.cells);
|
||||
try testing.expect(!f.elided);
|
||||
|
||||
// And a cut lands on a codepoint boundary, never mid-sequence.
|
||||
const cut = font.fit("ünïx", 3);
|
||||
try testing.expect(cut.elided);
|
||||
try testing.expectEqualStrings("ün", "ünïx"[0..cut.end]);
|
||||
}
|
||||
|
||||
test "malformed utf-8 in a title yields one glyph per bad byte" {
|
||||
var it: font.Iterator = .{ .bytes = "a\xffb\xc3" };
|
||||
try testing.expectEqual(@as(?u21, 'a'), it.next());
|
||||
try testing.expectEqual(@as(?u21, 0xfffd), it.next());
|
||||
try testing.expectEqual(@as(?u21, 'b'), it.next());
|
||||
// A truncated sequence at the end must terminate the walk, not loop.
|
||||
try testing.expectEqual(@as(?u21, 0xfffd), it.next());
|
||||
try testing.expectEqual(@as(?u21, null), it.next());
|
||||
}
|
||||
|
||||
test "text measuring and cell fitting agree" {
|
||||
const scale = 2;
|
||||
// Whatever cellsForWidth says fits must actually fit.
|
||||
var width: i32 = 0;
|
||||
while (width < 200) : (width += 1) {
|
||||
const cells = font.cellsForWidth(width, scale);
|
||||
try testing.expect(font.textWidth(cells, scale) <= width);
|
||||
try testing.expect(font.textWidth(cells + 1, scale) > width);
|
||||
}
|
||||
}
|
||||
|
||||
test "printable ascii all have glyphs, and unknown codepoints fall back" {
|
||||
// The space is blank, and nothing else printable is.
|
||||
var c: u8 = 0x21;
|
||||
while (c <= 0x7e) : (c += 1) {
|
||||
const rows = font.glyph(c);
|
||||
var lit = false;
|
||||
for (rows) |r| lit = lit or r != 0;
|
||||
try testing.expect(lit);
|
||||
// Nothing may spill outside the five column cell.
|
||||
for (rows) |r| try testing.expectEqual(@as(u8, 0), r & ~@as(u8, 0b11111));
|
||||
}
|
||||
for (font.glyph(' ')) |r| try testing.expectEqual(@as(u8, 0), r);
|
||||
|
||||
// Control characters stay blank; other codepoints get the fallback box.
|
||||
for (font.glyph('\n')) |r| try testing.expectEqual(@as(u8, 0), r);
|
||||
try testing.expect(!std.mem.eql(u8, font.glyph('?'), font.glyph('あ')));
|
||||
try testing.expectEqualSlices(u8, font.glyph(0xfffd), font.glyph('あ'));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user