Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ jobs:
echo "::endgroup::"
done
zig build -Dshape=studio -Dzon-layout=true --summary all
- name: Build the replay app
run: zig build replay --summary all
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ Each is a whole fizzy app: fizzy's frame, its built-in plugins (files, text, ima
in a layout of this repo's own, with two more plugins bundled in. Each shape gets its own
executable, window title and config directory (`minimalapp`, `studioapp`, `endlessapp`).

`zig build run-replay` is a plain dvui app, nothing of fizzy, with tape playback from the plugin
SDK's `replay` (`replay/main.zig`). At launch it plays a live tape into its own window (clicks,
typing, a command), draws the tape's pointer, stops if you click or type, and when the tape ends
prints what is on screen as text: the names a test or an automation client reads instead of
pixels. It depends only on the SDK package.

`zig build run-dvui` is where a plain dvui app on fizzy's backend goes: dvui's own widgets, with
floating windows, menus and dialogs as OS windows of their own (Liquid Glass windows on macOS).
It needs fizzy's backend as a package an app can depend on without the rest of fizzy, which is
Expand Down
28 changes: 28 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
//! `zig build run-fizzy` builds a fizzy app: fizzy's framework and its plugins, in one of the
//! shapes in `shapes/` (`-Dshape=`), with this repo's plugins bundled in.
//!
//! `zig build run-replay` builds a plain dvui app, nothing of fizzy, with tape playback from the
//! plugin SDK's `replay`: it plays a tape into its own window and prints what is on screen.
//!
//! `zig build run-dvui` will build a plain dvui app on fizzy's backend: dvui's widgets, with
//! floating windows, menus and dialogs as OS windows of their own. It needs the backend as a
//! package of its own first.
const std = @import("std");
const fizzy = @import("fizzy");
const fizzy_sdk = @import("fizzy_sdk");

/// The layout shapes. Each is one file in `shapes/`, read end to end and copied, never
/// configured: a shape that needs something new is a new file.
Expand Down Expand Up @@ -67,6 +71,30 @@ pub fn build(b: *std.Build) !void {
if (b.args) |args| run_fizzy.addArgs(args);
b.step("run-fizzy", "Run the fizzy app in the shape -Dshape picks").dependOn(&run_fizzy.step);

// A plain dvui app with tape playback: dvui as any app has it (the SDK pins it, the app picks
// its own backend) and `tape` and `replay` built against it. Not part of the default install.
const sdk = b.dependency("fizzy_sdk", .{ .target = target, .optimize = optimize });
const dvui_dep = sdk.builder.dependency("dvui", .{ .target = target, .optimize = optimize, .backend = .sdl3 });
const dvui_mod = dvui_dep.module("dvui_sdl3");
const automation = fizzy_sdk.replay.modules(b, sdk.builder, dvui_mod, target, optimize);
const replay_exe = b.addExecutable(.{
.name = "replay-app",
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("replay/main.zig"),
}),
});
replay_exe.root_module.addImport("dvui", dvui_mod);
replay_exe.root_module.addImport("tape", automation.tape);
replay_exe.root_module.addImport("replay", automation.replay);
const install_replay = b.addInstallArtifact(replay_exe, .{});
b.step("replay", "Build the replay app").dependOn(&install_replay.step);
const run_replay = b.addRunArtifact(replay_exe);
run_replay.step.dependOn(&install_replay.step);
if (b.args) |args| run_replay.addArgs(args);
b.step("run-replay", "Run the replay app: a plain dvui app with tape playback").dependOn(&run_replay.step);

// Where the dvui app goes: dvui's own widgets on fizzy's backend. Waits on the backend
// becoming a package an app depends on without the rest of fizzy.
b.step("run-dvui", "Run a plain dvui app on fizzy's backend (not yet)").dependOn(
Expand Down
12 changes: 9 additions & 3 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
// by commit; fizzy's CI builds this repo against each of its pull requests, so a change
// there that breaks an outside app fails before it lands.
.fizzy = .{
.url = "https://github.com/fizzyedit/fizzy/archive/a2b4e0a7d6b3.tar.gz",
.hash = "fizzy-0.2.0-c46aVokhiwCxp5KQ-d44REid0496AzQ8hGP_DHEcyykr",
.url = "https://github.com/fizzyedit/fizzy/archive/4649893e4d27.tar.gz",
.hash = "fizzy-0.2.0-c46aVrUuiwDNap4KtjKj7wmmReYkOlSa5z4p7hDBUCgu",
},
// The plugins this app bundles beyond fizzy's own: fizzyedit/example-plugin, as any
// plugin from its own repo, and this app's own, which also builds alone as a dylib
Expand All @@ -19,6 +19,12 @@
.hash = "hello-0.1.0-c1Gg4rQXAAA7Ea4vZqD4jAS7eLtXJW3QglYM4HB-17Lw",
},
.shader = .{ .path = "plugins/shader" },
// The plugin SDK alone, for the replay app: its pinned dvui and `tape` / `replay`, with
// nothing of fizzy the app.
.fizzy_sdk = .{
.url = "https://github.com/fizzyedit/fizzy/releases/download/sdk-v0.2.19/fizzy-sdk-v0.2.19.tar.gz",
.hash = "fizzy-0.2.19-SGBwY8MiIQDGX5QO1AMQHemD99HG9DNnjTLQ4Pu1bpf5",
},
},
.paths = .{ "build.zig", "build.zig.zon", "shapes", "plugins", "README.md", "LICENSE" },
.paths = .{ "build.zig", "build.zig.zon", "shapes", "plugins", "replay", "README.md", "LICENSE" },
}
123 changes: 123 additions & 0 deletions replay/main.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! A plain dvui app with tape playback, and nothing of fizzy's.
//!
//! At launch it plays a live tape into its own window: two clicks on Count, a click into the field
//! and some typing, a command that resets the count, and one more click — so it ends on 1. The tape aims at widgets by their tags, so it plays at any
//! window size; the tape's pointer and its clicks are drawn over the app (`replay.overlay`); a
//! click or key from a person stops it. When it ends, the app prints what is on screen, as text
//! (`replay.Snapshot`) — the names a test or an automation client reads instead of pixels.
//!
//! What an app gives `replay`: a `Stage` (`idle` and `command` are enough for a live tape),
//! `LiveDriver.frame` and `Snapshot.beginFrame` first thing in its frame, and the overlay and
//! `Snapshot.endFrame` last.
const std = @import("std");
const dvui = @import("dvui");
const replay = @import("replay");
const Script = @import("tape").Script;

pub const dvui_app: dvui.App = .{
.config = .{ .options = .{
.size = .{ .w = 520, .h = 360 },
.title = "Replay example",
} },
.frameFn = frame,
.initFn = init,
.deinitFn = deinit,
};
pub const main = dvui.App.main;
pub const panic = dvui.App.panic;
pub const std_options: std.Options = .{ .logFn = dvui.App.logFn };

var gpa: std.mem.Allocator = undefined;
var count: usize = 0;
var driver: replay.LiveDriver = undefined;
var snapshot: replay.Snapshot = undefined;
/// The snapshot asked for when the last tape ended, until it is printed.
var shot: ?u32 = null;
var played_once = false;
/// The last tape's end has been read back.
var reported = true;

// ---- the stage: what this app is to a tape -------------------------------------------------

const stage_vtable: replay.Stage.VTable = .{ .idle = idle, .command = command };

/// Nothing here loads in the background: always settled.
fn idle(_: *anyopaque) bool {
return true;
}

/// The one command this app offers a tape.
fn command(_: *anyopaque, id: []const u8, _: []const u8) void {
if (std.mem.eql(u8, id, "example.reset")) count = 0;
}

// ---- the app -------------------------------------------------------------------------------

fn init(win: *dvui.Window) !void {
gpa = win.gpa;
driver = .init(.{ .ctx = &count, .vtable = &stage_vtable });
snapshot = .init(gpa);
}

fn deinit(_: *dvui.Window) void {
driver.deinit();
snapshot.deinit();
}

/// The tape: written with `tape.Script`, aimed at tags rather than positions.
fn demoTape() !@import("tape").Tape.Owned {
var s: Script = .init(gpa, "example", "Count, type, reset");
errdefer s.deinit();
s.check = replay.Input.check;
s.check.live = true;
try s.click(.{ .tag = "example.count" }, .{});
try s.click(.{ .tag = "example.count" }, .{});
try s.click(.{ .tag = "example.field", .x = 0.5, .y = 0.5 }, .{});
try s.typeText("typed by a tape", .{});
s.pause(600);
try s.command("example.reset");
try s.click(.{ .tag = "example.count" }, .{});
return s.finish();
}

fn play() void {
driver.play(demoTape() catch return) catch |err| return std.log.err("tape refused: {t}", .{err});
reported = false;
}

fn frame() !dvui.App.Result {
// First, before anything reads the frame's events.
snapshot.beginFrame();
driver.frame();
if (!played_once) {
played_once = true;
play();
}

{
var box = dvui.box(@src(), .{ .dir = .vertical }, .{ .expand = .both, .padding = .all(16), .background = true, .style = .window });
defer box.deinit();
dvui.label(@src(), "Clicked {d} times", .{count}, .{});
if (dvui.button(@src(), "Count", .{}, .{ .tag = "example.count" })) count += 1;
var entry = dvui.textEntry(@src(), .{}, .{ .tag = "example.field", .expand = .horizontal });
entry.deinit();
if (dvui.button(@src(), "Play the tape again", .{}, .{}) and !driver.playing()) play();
if (driver.outcome) |o| dvui.label(@src(), "Last tape: {t}", .{o}, .{});
}

// When a tape ends, read the screen back as text.
if (!driver.playing() and !reported) {
reported = true;
std.debug.print("The tape ended ({any}); the count is {d}.\n", .{ driver.outcome, count });
shot = snapshot.request();
}
if (shot) |n| if (snapshot.get(n)) |text| {
std.debug.print("What is on screen:\n{s}", .{text});
shot = null;
};

// Last, over everything: the tape's pointer, then the snapshot written out.
replay.overlay.drawLive(&driver, .{});
snapshot.endFrame();
return .ok;
}
Loading