creek

A malleable and minimalist status bar for the River compositor

git clone https://git.8pit.net/creek.git

  1const std = @import("std");
  2const mem = std.mem;
  3const unicode = std.unicode;
  4
  5const fcft = @import("fcft");
  6const pixman = @import("pixman");
  7const time = @cImport(@cInclude("time.h"));
  8
  9const Buffer = @import("Buffer.zig");
 10const Bar = @import("Bar.zig");
 11const Tag = @import("Tags.zig").Tag;
 12
 13const state = &@import("root").state;
 14
 15pub const RenderFn = fn (*Bar) anyerror!void;
 16
 17pub fn toUtf8(gpa: mem.Allocator, bytes: []const u8) ![]u32 {
 18    const utf8 = try unicode.Utf8View.init(bytes);
 19    var iter = utf8.iterator();
 20
 21    var runes = try std.ArrayList(u32).initCapacity(gpa, bytes.len);
 22    var i: usize = 0;
 23    while (iter.nextCodepoint()) |rune| : (i += 1) {
 24        runes.appendAssumeCapacity(rune);
 25    }
 26
 27    return runes.toOwnedSlice();
 28}
 29
 30pub fn renderTags(bar: *Bar) !void {
 31    const surface = bar.tags.surface;
 32    const tags = bar.monitor.tags.tags;
 33
 34    const buffers = &bar.tags.buffers;
 35    const shm = state.wayland.shm.?;
 36
 37    const width = bar.height * @as(u16, tags.len + 1);
 38    const buffer = try Buffer.nextBuffer(buffers, shm, width, bar.height);
 39    if (buffer.buffer == null) return;
 40    buffer.busy = true;
 41
 42    for (&tags, 0..) |*tag, i| {
 43        const offset: i16 = @intCast(bar.height * i);
 44        try renderTag(buffer.pix.?, tag, bar.height, offset);
 45    }
 46
 47    // Separator tag to visually separate last focused tag from
 48    // focused window title (both use the same background color).
 49    const offset: i16 = @intCast(bar.height * tags.len);
 50    try renderTag(buffer.pix.?, &Tag{ .label = '|' }, bar.height, offset);
 51
 52    bar.tags_width = width;
 53    surface.setBufferScale(bar.monitor.scale);
 54    surface.damageBuffer(0, 0, width, bar.height);
 55    surface.attach(buffer.buffer, 0, 0);
 56}
 57
 58fn renderRun(start: i32, buffer: *Buffer, image: *pixman.Image, bar: *Bar, glyphs: [*]*const fcft.Glyph, count: usize) !i32 {
 59    const font_height: u32 = @intCast(state.config.font.height);
 60    const y_offset: i32 = @intCast((bar.height - font_height) / 2);
 61
 62    var i: usize = 0;
 63    var x: i32 = start;
 64    while (i < count) : (i += 1) {
 65        const glyph = glyphs[i];
 66        x += @intCast(glyph.x);
 67        const y = (state.config.font.ascent - @as(i32, @intCast(glyph.y))) + y_offset;
 68        pixman.Image.composite32(.over, image, glyph.pix, buffer.pix.?, 0, 0, 0, 0, x, y, glyph.width, glyph.height);
 69        x += glyph.advance.x - @as(i32, @intCast(glyph.x));
 70    }
 71
 72    return x;
 73}
 74
 75pub fn renderTitle(bar: *Bar, title: ?[]const u8) !void {
 76    const surface = bar.title.surface;
 77    const shm = state.wayland.shm.?;
 78
 79    var runes: ?[]u32 = null;
 80    if (title) |t| {
 81        if (t.len > 0)
 82            runes = try toUtf8(state.gpa, t);
 83    }
 84    defer {
 85        if (runes) |r| state.gpa.free(r);
 86    }
 87
 88    // calculate width
 89    const title_start = bar.tags_width;
 90    const text_start = if (bar.text_width == 0) blk: {
 91        break :blk 0;
 92    } else blk: {
 93        break :blk bar.width - bar.text_width - bar.text_padding;
 94    };
 95    const width: u16 = if (text_start > 0) blk: {
 96        break :blk @intCast(text_start - title_start - bar.text_padding);
 97    } else blk: {
 98        break :blk bar.width - title_start;
 99    };
100
101    // set subsurface offset
102    const x_offset = bar.tags_width;
103    const y_offset = 0;
104    bar.title.subsurface.setPosition(x_offset, y_offset);
105
106    const buffers = &bar.title.buffers;
107    const buffer = try Buffer.nextBuffer(buffers, shm, width, bar.height);
108    if (buffer.buffer == null) return;
109    buffer.busy = true;
110
111    var bg_color = state.config.normalBgColor;
112    if (title) |t| {
113        if (t.len > 0) bg_color = state.config.focusBgColor;
114    }
115    const bg_area = [_]pixman.Rectangle16{
116        .{ .x = 0, .y = 0, .width = width, .height = bar.height },
117    };
118    _ = pixman.Image.fillRectangles(.src, buffer.pix.?, &bg_color, 1, &bg_area);
119
120    if (runes) |r| {
121        const font = state.config.font;
122        const run = try font.rasterizeTextRunUtf32(r, .default);
123        defer run.destroy();
124
125        // calculate maximum amount of glyphs that can be displayed
126        var max_x: i32 = bar.text_padding;
127        var max_glyphs: u16 = 0;
128        var i: usize = 0;
129        while (i < run.count) : (i += 1) {
130            const glyph = run.glyphs[i];
131            max_x += @intCast(glyph.x);
132            if (max_x >= width - (2 * bar.text_padding) - bar.abbrev_width) {
133                break;
134            }
135            max_x += glyph.advance.x - @as(i32, @intCast(glyph.x));
136            max_glyphs += 1;
137        }
138
139        var x: i32 = bar.text_padding;
140        const color = pixman.Image.createSolidFill(&state.config.focusFgColor).?;
141        x += try renderRun(bar.text_padding, buffer, color, bar, run.glyphs, max_glyphs);
142        if (run.count > max_glyphs) { // if abbreviated
143            _ = try renderRun(x, buffer, color, bar, bar.abbrev_run.glyphs, bar.abbrev_run.count);
144        }
145    }
146
147    surface.setBufferScale(bar.monitor.scale);
148    surface.damageBuffer(0, 0, width, bar.height);
149    surface.attach(buffer.buffer, 0, 0);
150}
151
152pub fn resetText(bar: *Bar) !void {
153    const surface = bar.text.surface;
154    const shm = state.wayland.shm.?;
155
156    const buffers = &bar.text.buffers;
157    const buffer = try Buffer.nextBuffer(buffers, shm, bar.text_width, bar.height);
158    if (buffer.buffer == null) return;
159    buffer.busy = true;
160
161    const text_to_bottom: u16 =
162        @intCast(state.config.font.height + bar.text_padding);
163    const bg_area = [_]pixman.Rectangle16{
164        .{ .x = 0, .y = 0, .width = bar.text_width, .height = text_to_bottom },
165    };
166    var bg_color = state.config.normalBgColor;
167    _ = pixman.Image.fillRectangles(.src, buffer.pix.?, &bg_color, 1, &bg_area);
168
169    surface.setBufferScale(bar.monitor.scale);
170    surface.damageBuffer(0, 0, bar.text_width, bar.height);
171    surface.attach(buffer.buffer, 0, 0);
172}
173
174pub fn renderText(bar: *Bar, text: []const u8) !void {
175    const surface = bar.text.surface;
176    const shm = state.wayland.shm.?;
177
178    // utf8 encoding
179    const runes = try toUtf8(state.gpa, text);
180    defer state.gpa.free(runes);
181
182    // rasterize
183    const font = state.config.font;
184    const run = try font.rasterizeTextRunUtf32(runes, .default);
185    defer run.destroy();
186
187    // compute total width
188    var i: usize = 0;
189    var width: u16 = 0;
190    while (i < run.count) : (i += 1) {
191        width += @intCast(run.glyphs[i].advance.x);
192    }
193
194    // set subsurface offset
195    const font_height: u32 = @intCast(state.config.font.height);
196    const x_offset: i32 = @intCast(bar.width - width - bar.text_padding);
197    const y_offset: i32 = @intCast(@divFloor(bar.height - font_height, 2));
198    bar.text.subsurface.setPosition(x_offset, y_offset);
199
200    const buffers = &bar.text.buffers;
201    const buffer = try Buffer.nextBuffer(buffers, shm, width, bar.height);
202    if (buffer.buffer == null) return;
203    buffer.busy = true;
204
205    const bg_area = [_]pixman.Rectangle16{
206        .{ .x = 0, .y = 0, .width = width, .height = bar.height },
207    };
208    const bg_color = mem.zeroes(pixman.Color);
209    _ = pixman.Image.fillRectangles(.src, buffer.pix.?, &bg_color, 1, &bg_area);
210
211    var x: i32 = 0;
212    i = 0;
213    const color = pixman.Image.createSolidFill(&state.config.normalFgColor).?;
214    while (i < run.count) : (i += 1) {
215        const glyph = run.glyphs[i];
216        x += @intCast(glyph.x);
217        const y = state.config.font.ascent - @as(i32, @intCast(glyph.y));
218        pixman.Image.composite32(.over, color, glyph.pix, buffer.pix.?, 0, 0, 0, 0, x, y, glyph.width, glyph.height);
219        x += glyph.advance.x - @as(i32, @intCast(glyph.x));
220    }
221
222    surface.setBufferScale(bar.monitor.scale);
223    surface.damageBuffer(0, 0, width, bar.height);
224    surface.attach(buffer.buffer, 0, 0);
225
226    // render title again if text width changed
227    if (width != bar.text_width) {
228        bar.text_width = width;
229
230        if (state.wayland.river_seat) |seat| {
231            seat.mtx.lock();
232            defer seat.mtx.unlock();
233
234            try renderTitle(bar, seat.window_title);
235            bar.title.surface.commit();
236            bar.background.surface.commit();
237        }
238    }
239}
240
241fn renderTag(
242    pix: *pixman.Image,
243    tag: *const Tag,
244    size: u16,
245    offset: i16,
246) !void {
247    const outer = [_]pixman.Rectangle16{
248        .{ .x = offset, .y = 0, .width = size, .height = size },
249    };
250    const outer_color = tag.bgColor();
251    _ = pixman.Image.fillRectangles(.over, pix, outer_color, 1, &outer);
252
253    if (tag.occupied) {
254        const font_height: u16 = @intCast(state.config.font.height);
255
256        // Constants taken from dwm-6.3 drawbar function.
257        const boxs: i16 = @intCast(font_height / 9);
258        const boxw: u16 = font_height / 6 + 2;
259
260        const box = pixman.Rectangle16{
261            .x = offset + boxs,
262            .y = boxs,
263            .width = boxw,
264            .height = boxw,
265        };
266
267        const box_color = if (tag.focused) blk: {
268            break :blk &state.config.normalBgColor;
269        } else blk: {
270            break :blk tag.fgColor();
271        };
272
273        _ = pixman.Image.fillRectangles(.over, pix, box_color, 1, &[_]pixman.Rectangle16{box});
274        if (!tag.focused) {
275            const border = 1; // size of the border
276            const inner = pixman.Rectangle16{
277                .x = box.x + border,
278                .y = box.y + border,
279                .width = box.width - (2 * border),
280                .height = box.height - (2 * border),
281            };
282
283            const inner_color = tag.bgColor();
284            _ = pixman.Image.fillRectangles(.over, pix, inner_color, 1, &[_]pixman.Rectangle16{inner});
285        }
286    }
287
288    const glyph_color = tag.fgColor();
289    const font = state.config.font;
290    const char = pixman.Image.createSolidFill(glyph_color).?;
291    const glyph = try font.rasterizeCharUtf32(tag.label, .default);
292    const x = offset + @divFloor(size - glyph.width, 2);
293    const y = @divFloor(size - glyph.height, 2);
294    pixman.Image.composite32(.over, char, glyph.pix, pix, 0, 0, 0, 0, x, y, glyph.width, glyph.height);
295}