Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
6774f57
app: the crash reporter's docs say a plugin's panic is forwarded (#436)
foxnne Oct 15, 2026
3fe6d6f
sdk: release 0.2.27 (#439)
foxnne Oct 15, 2026
0129bdb
zig: builtin.target.os, .cpu and .abi in place of builtin.os (#435)
foxnne Oct 15, 2026
641b76a
ci: check-examples packs without macOS AppleDouble files (#443)
foxnne Oct 15, 2026
b9ba48a
build: an app lists the fizzy plugins it links (#441)
foxnne Oct 15, 2026
b169188
sdk: a plugin can open a document beside the person (#442)
foxnne Oct 15, 2026
09e13c1
app: an untitled document survives a crash or a restart (#447)
foxnne Oct 15, 2026
5651df9
core: a kept frost reads again under a region's swap (#450)
foxnne Oct 16, 2026
523a89f
core: a canvas the person has not moved fits its pane; Fit to View pu…
foxnne Oct 16, 2026
a254595
core: viewport buttons are glass bubbles; Fit to View is one (#446)
foxnne Oct 16, 2026
1265f60
sdk: the file tree keeps its own folders; files.reveal shows new file…
foxnne Oct 16, 2026
760ea13
core: a row carried out of a tree is the OS's glass, and stays still …
foxnne Oct 16, 2026
0dfdb01
markdown: an inline render no longer shrinks for a frame as it change…
foxnne Oct 16, 2026
c45686c
ci: compiled shaders are built in CI and checked against their HLSL (…
foxnne Oct 16, 2026
07d8b03
app: a link-only build, which loads no plugin at runtime (#445)
foxnne Oct 16, 2026
21e4ccb
tape: steps of a document's history, and a deflated binary form (#453)
foxnne Oct 16, 2026
357a063
shaders: the glass written once, its Metal and GLSL generated (#424)
foxnne Oct 16, 2026
18ce8bb
docs: Fizzy is MIT again; licensing and trademark policy (#440)
foxnne Oct 16, 2026
2180f21
sdk: owners report a document's steps, and apply recorded ones (#456)
foxnne Oct 16, 2026
ae3d39f
tape: a live tape with no ops finishes instead of crashing (#457)
foxnne Oct 16, 2026
22d397c
app: a refused plugin says whether its mode or its SDK shape differs …
foxnne Oct 16, 2026
d1f3586
sdk: release 0.2.28 (#455)
foxnne Oct 16, 2026
66040f9
app: a track per document, recorded behind FIZZY_TRACKS (#459)
foxnne Oct 16, 2026
f6d248b
app: workspaces are views of the app's, behind FIZZY_WORKSPACES (#461)
foxnne Oct 16, 2026
fc1246f
app: tracks measured, with delta checkpoints and a cap (#460)
foxnne Oct 16, 2026
fdb212e
layout: a document lands in any place as a view would (#462)
foxnne Oct 16, 2026
6b527e7
layout: the region picker groups views by the place they are made for…
foxnne Oct 16, 2026
68f6f97
layout: the picker's buttons are named for tapes and agents (#468)
foxnne Oct 16, 2026
ebada7e
layout: a split of a carded place opens even halves (#465)
foxnne Oct 16, 2026
8a8eac7
layout: removing a half that was split again keeps its column (#467)
foxnne Oct 16, 2026
83304c2
layout: the picker lists a view under the place showing it (#470)
foxnne Oct 16, 2026
f6a8f2d
app: workspaces carry an old panes.zon over into the layout (#464)
foxnne Oct 16, 2026
88233a9
layout: test the picker's empty header (#473)
foxnne Oct 16, 2026
3a8031f
app: undo and redo show a document's transport (#472)
foxnne Oct 16, 2026
ef30c0c
sdk: who made each step: Author, act, and the host's author map (#474)
foxnne Oct 16, 2026
6de2df8
app: whose frame it is, and bubbles that say who (#475)
foxnne Oct 16, 2026
0d071c3
layout: a pane writing its tabs back no longer reads freed ids (#476)
foxnne Oct 17, 2026
2606073
text: every step is reported, with the top, and only one hand's typin…
foxnne Oct 17, 2026
6eec457
sdk: release 0.2.29 (#477)
foxnne Oct 17, 2026
7b538aa
ci: compile the unit tests before running them (#469)
foxnne Oct 17, 2026
46ddcd8
zig/0.17: merge main (40 commits, to #469)
foxnne Oct 17, 2026
0ee96b6
app: a document's track exported as a tape, and played (#478)
foxnne Oct 17, 2026
3f7b2ff
app: fizzy.undo and fizzy.redo take a path and the step expected (#487)
foxnne Oct 17, 2026
e386e86
app: a tape plays in a tab of its own, and the transport shares (#485)
foxnne Oct 17, 2026
83001ec
app: share a range of a track, after a summary of what it holds (#493)
foxnne Oct 17, 2026
de7152f
app: trim a shared tape in its tab, as macOS trims a recording (#497)
foxnne Oct 17, 2026
c8bbbe9
app: control server on a Unix socket, commands over HTTP (#490)
foxnne Oct 17, 2026
0be3e64
zig/0.17: record #486's merge of main as a parent
foxnne Oct 18, 2026
47a0c1e
zig/0.17: merge main (6 commits, to #490)
foxnne Oct 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,12 @@ each directory is.
springs, easing or trails driven by the pointer: merged Liquid Glass necks re-form visibly
under tremor. A one-shot, time-driven curve started by a discrete event (the aimed bubble
changing) is fine.
- **std offers AF_UNIX on Windows only to a target whose minimum is 10 1803** (`win10_rs4`,
`std.Io.net.has_unix_sockets`); below it, listening fails with `AddressFamilyUnsupported`
whatever the machine runs. Fizzy's `build.zig` raises the minimum for the control server
(`control/`); an app built on fizzy with its own target must too, or its server does not start.
On Windows the socket is a reparse point `statFile` cannot open, so its inode is not checked.
- **End a blocked `accept` by cancelling its `Io` task, never by closing the listener.** std's
Windows `accept` treats the cancelled wait that closing causes as `unreachable` (a panic), and
connecting to the socket's path to wake it reaches whoever listens there now. `Future.cancel`
interrupts it on every OS (`control/Server.zig`).
4 changes: 3 additions & 1 deletion app/ActivityFeed.zig
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ pub fn clear(self: *ActivityFeed) void {
self.len = 0;
}

fn push(self: *ActivityFeed, title: []const u8, now: i128) void {
/// Show `title` as though a command of that name had just run: how the app says something
/// about the document itself (a share kept, a tape stopped) in the same glass as its commands.
pub fn push(self: *ActivityFeed, title: []const u8, now: i128) void {
if (self.len == max) {
std.mem.copyForwards(Item, self.items[0 .. max - 1], self.items[1..max]);
self.len -= 1;
Expand Down
6 changes: 6 additions & 0 deletions app/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const Layout = @import("layout/Layout.zig");
const Host = sdk.Host;
const PluginLoader = @import("root.zig").store.Loader;
const SettingsWatcher = @import("watch/SettingsWatcher.zig");
const ControlServer = @import("control/Server.zig");
const ExecutableWatcher = @import("watch/ExecutableWatcher.zig");
const FolderWatcher = @import("watch/FolderWatcher.zig");
const Keymap = @import("keymap/Keymap.zig");
Expand Down Expand Up @@ -260,6 +261,11 @@ settings_save_deadline_ns: i128 = 0,
/// `SettingsWatcher.start`'s doc comment), torn down first in `deinit`.
settings_watcher: ?SettingsWatcher = null,

/// The control server (`app.control`), while it runs: started in `postInit` when `FIZZY_CONTROL=1`
/// (plans/CONTROL_SERVER_PLAN.md, steps 1–3 land dark), answered each frame, stopped at quit.
/// Always void on wasm, which has no socket.
control: if (builtin.target.cpu.arch == .wasm32) void else ?ControlServer = if (builtin.target.cpu.arch == .wasm32) {} else null,

/// Watches the app's own executable while `settings.restart_on_rebuild` is on: a rebuild restarts
/// the app into it, keeping every document. Null while the setting is off, and wherever
/// `SettingsWatcher` would be.
Expand Down
45 changes: 35 additions & 10 deletions app/HistoryTransport.zig
Original file line number Diff line number Diff line change
Expand Up @@ -62,26 +62,41 @@ fn stopId(line: sdk.Plugin.HistoryLine, i: usize) u64 {
return if (i == 0) 0 else line.entries[i - 1].id;
}

pub const DrawOptions = struct {
/// A share button at the bar's end: the app has a way to share how the document was made
/// (fizzy: its track as a tape). What a press does is the app's (`Pressed.share`).
share: bool = false,
};

/// What was pressed on the bar this frame that is the app's to act on.
pub const Pressed = struct {
share: bool = false,
};

/// Draw the bar over `area` (the document's pane, physical) when it shows, and take its input.
/// `id_extra` keeps one pane's bar apart from another's.
pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physical, id_extra: usize) void {
pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physical, id_extra: usize, opts: DrawOptions) Pressed {
var pressed: Pressed = .{};
const now = dvui.frameTimeNS();
// The pointer on it, or a drag in it, keeps it.
if (self.drag != null) self.shown.stir(now) else if (self.place(area, now)) |b| if (b.contains(dvui.currentWindow().mouse_pt)) self.shown.stir(now);
const p = self.shown.openness(now);
if (p <= 0) return;
if (p <= 0) return pressed;
const arena = dvui.currentWindow().arena();
const line = doc.owner.historyLine(doc, arena) orelse return;
const line = doc.owner.historyLine(doc, arena) orelse return pressed;
const n = line.entries.len;

const wrs = dvui.windowRectScale();
const s = wrs.s;
const whole = geometry(area.toNatural()) orelse return;
const whole = geometry(area.toNatural()) orelse return pressed;
const w = whole.w;
const center = whole.center();
// Share, when there is a way to: the last bubble, inside the bar's width.
const end_w: f32 = if (opts.share) button_d + gap else 0;
const back: dvui.Rect = .{ .x = whole.x, .y = center.y - button_d / 2, .w = button_d, .h = button_d };
const fwd: dvui.Rect = .{ .x = whole.x + w - button_d, .y = back.y, .w = button_d, .h = button_d };
const track: dvui.Rect = .{ .x = back.x + button_d + gap, .y = whole.y, .w = w - 2 * (button_d + gap), .h = bar_h };
const fwd: dvui.Rect = .{ .x = whole.x + w - button_d - end_w, .y = back.y, .w = button_d, .h = button_d };
const share: dvui.Rect = .{ .x = whole.x + w - button_d, .y = back.y, .w = if (opts.share) button_d else 0, .h = button_d };
const track: dvui.Rect = .{ .x = back.x + button_d + gap, .y = whole.y, .w = w - 2 * (button_d + gap) - end_w, .h = bar_h };
const hit = wrs.rectToPhysical(whole.outsetAll(4));

var float: dvui.FloatingWidget = undefined;
Expand All @@ -93,7 +108,7 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
// and went), it holds nothing, and the knob is the document's again.
if (self.drag != null and !dvui.captured(fid)) self.drag = null;

const phys = [3]dvui.Rect.Physical{ wrs.rectToPhysical(back), wrs.rectToPhysical(track), wrs.rectToPhysical(fwd) };
const phys = [4]dvui.Rect.Physical{ wrs.rectToPhysical(back), wrs.rectToPhysical(track), wrs.rectToPhysical(fwd), wrs.rectToPhysical(share) };
const pad = 14;
const x0 = phys[1].x + pad * s;
const x1 = phys[1].x + phys[1].w - pad * s;
Expand All @@ -106,6 +121,7 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
core.anchor.markRect(fid, phys[0], true, "history.back:{s}", .{path});
core.anchor.markRect(fid, .{ .x = x0, .y = phys[1].y, .w = x1 - x0, .h = phys[1].h }, true, "history.line:{s}", .{path});
core.anchor.markRect(fid, phys[2], true, "history.forward:{s}", .{path});
if (opts.share) core.anchor.markRect(fid, phys[3], true, "history.share:{s}", .{path});

// Input: the buttons step, the line goes where it is pressed and follows a drag.
var go: ?usize = null;
Expand All @@ -122,6 +138,8 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
go = line.at -| 1;
} else if (phys[2].contains(me.p)) {
go = @min(n, line.at + 1);
} else if (opts.share and phys[3].contains(me.p)) {
pressed.share = true;
} else if (line_hit.contains(me.p)) {
dvui.captureMouse(float.data(), e.num);
self.drag = rules.stopAt(me.p.x, x0, x1, n);
Expand Down Expand Up @@ -149,16 +167,22 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
const prev_alpha = dvui.alpha(p);
defer dvui.alphaSet(prev_alpha);
const pt = dvui.currentWindow().mouse_pt;
var bubbles: [3]core.dialogs.Bubble = undefined;
var bubbles: [4]core.dialogs.Bubble = undefined;
for (phys, &bubbles, 0..) |r, *b, i| b.* = .{
.rect = r,
.light = if (i != 1 and r.contains(pt) and (if (i == 0) at > 0 else at < n)) core.dialogs.bubble_hover else 0,
.light = if (r.contains(pt) and switch (i) {
0 => at > 0,
2 => at < n,
3 => true,
else => false,
}) core.dialogs.bubble_hover else 0,
};
core.dialogs.glassBubbles(fid, &bubbles, s, .{ .merge = 14, .shadow = 1 });
core.dialogs.glassBubbles(fid, bubbles[0..if (opts.share) 4 else 3], s, .{ .merge = 14, .shadow = 1 });

const ink = dvui.themeGet().color(.window, .text);
button(bounds, back, "history_back", icons.tvg.lucide.undo, ink.opacity(if (at > 0) 0.85 else 0.3));
button(bounds, fwd, "history_forward", icons.tvg.lucide.redo, ink.opacity(if (at < n) 0.85 else 0.3));
if (opts.share) button(bounds, share, "history_share", icons.tvg.lucide.@"share-2", ink.opacity(0.85));

// What the document is at, over the line: the step's title, and how far along.
const font = dvui.Font.theme(.body).larger(-2);
Expand All @@ -178,6 +202,7 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
// own (it starts to go, it is gone).
if (p < 1) dvui.refresh(null, @src(), fid);
if (self.shown.nextChange(now)) |t| dvui.timer(fid, @intCast(@min(@divTrunc(t - now, 1000) + 1, std.math.maxInt(i32))));
return pressed;
}

/// A step button's icon, in the middle of its bubble `r` (window-natural; `bounds` the layer's).
Expand Down
Loading
Loading