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
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
on:
pull_request:
workflow_dispatch:
push:
branches:
- main
- master
tags:
- v?[0-9]+.[0-9]+.[0-9]+*

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
DeterminateCI:
uses: DeterminateSystems/ci/.github/workflows/workflow.yml@main
permissions:
id-token: write
contents: read
12 changes: 6 additions & 6 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
.version = "0.1.0",
.minimum_zig_version = "0.16.0",
.dependencies = .{
.lattice = .{
.url = "git+https://github.com/Midstall/lattice#dc7ab4610a0b77167234af43e85b82ae99b63c15",
.hash = "lattice-0.1.0-TdtxHONlCQD42nhei2aw6kuqs7vRnA_mu4V_KvMDszjf",
},
.prism = .{
.url = "git+https://github.com/Midstall/prism#6b14a3e72bf05427e8ff75335f0206d079c508f0",
.hash = "prism-0.1.0-PSE12I20OAC4gr2XF_0RgEyXa0N0NvzYZyUsXI2VRkhR",
.url = "git+https://github.com/Midstall/prism#d50303f2302c161ff3e9fadbb24f990248fe1e0f",
.hash = "prism-0.1.0-PSE12N28OACyQoAXy3yrAXyMxthCSa6ur1TgjVpCL0c_",
},
.webidl = .{
.url = "git+https://github.com/Midstall/webidl.zig#4b0d3c61d5f22e6072e9eacf03174e4e8393c6a2",
.hash = "webidl_zig-0.1.0-FrIyV_uUBgAGtNwP-OkLegPJMWUnpeKuPqDdVbWyC9Fn",
},
.lattice = .{
.url = "git+https://github.com/Midstall/lattice#9f0a9b128dcf012e3760f440e4f5cd9562ec8752",
.hash = "lattice-0.1.0-TdtxHEviCQCoi6o8R_6d05KlMo2iVFGryVFpGbDEW81-",
},
},
.paths = .{ "build", "build.zig", "build.zig.zon", "lib" },
}
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@

zigDeps = pkgs.zig.fetchDeps {
inherit (finalAttrs) src pname version;
hash = "sha256-fVebraG0knvhYlTEZHH6ldCeVbB1KC1AR9LnBKD/L9c=";
hash = "sha256-8xkDtB/nAGOqPLghu4toAtQY0zEY8pzxWiwnbz99tfM=";
};

nativeBuildInputs = with pkgs; [
Expand Down
115 changes: 115 additions & 0 deletions lib/phantom/window.zig
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,37 @@ pub fn available(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process
return false;
}

/// Fold a lattice key event into the shared phantom key event. lattice has
/// already run the keymap: the keysym arrives as the X11 number input.Keysym
/// carries, so the conversion is a bit cast and not a table. The enum is
/// non-exhaustive, so any u32 lattice can send is a valid cast.
///
/// Analyzed only where the compositor path builds: `lattice` is `void`
/// elsewhere, and every reference to this function sits behind that gate.
fn keyEventFromLattice(k: lattice.event.KeyEvent) phantom.input.KeyEvent {
return .{
.keysym = @enumFromInt(k.keysym),
.text = k.text,
.mods = .{
.shift = k.mods.shift,
.ctrl = k.mods.ctrl,
.alt = k.mods.alt,
.super = k.mods.super,
},
.action = switch (k.state) {
.pressed => .press,
.released => .release,
},
};
}

/// Route one lattice key event into the focus manager. The window path has no
/// hard quit key: closing a window is the compositor's `close_requested`, and
/// an application's own shortcuts are what `KeyboardListener` is for.
fn dispatchKey(focus_mgr: *phantom.FocusManager, k: lattice.event.KeyEvent) void {
_ = focus_mgr.dispatch(keyEventFromLattice(k));
}

/// Where a window session draws, and how much of the process it takes over.
///
/// Every default matches what `App.run` did when it was the only way in: a
Expand Down Expand Up @@ -135,6 +166,11 @@ pub const Session = struct {
canvas: phantom.Canvas,

dispatcher: phantom.input.Dispatcher,
/// The focus manager owns keyboard traversal for the tree. It must outlive
/// the element tree: tearing the tree down walks every render object and
/// calls back into this to forget its focus handler, which is why `deinit`
/// takes the tree down first. Mirrors `tui.Session`.
focus_mgr: phantom.FocusManager,
/// The last pointer position, in physical pixels. A button and a scroll
/// report no position of their own, so both land wherever the last motion
/// left the pointer.
Expand Down Expand Up @@ -211,6 +247,14 @@ pub const Session = struct {
});
const view = self.owner.activeView().?;

// The focus manager must exist before the tree does: a failed mount
// tears the partial tree down through `errdefer`, and that walk calls
// back into the manager to forget each focus handler. Mirrors
// `tui.Session.init`.
self.focus_mgr = .{};
errdefer self.focus_mgr.deinit(gpa);
self.owner.focus = &self.focus_mgr;

var bctx = phantom.BuildContext{ .arena = self.arena.allocator(), .owner = &self.owner };
const root_widget = root.call(&bctx);
var mq = phantom.MediaQuery{ .data = &view.metrics, .child = root_widget };
Expand Down Expand Up @@ -240,6 +284,7 @@ pub const Session = struct {
pub fn deinit(self: *Session) void {
self.canvas.deinit();
self.el.deinit(self.gpa);
self.focus_mgr.deinit(self.gpa);
self.owner.deinit();
self.arena.deinit();
self.ctx.destroySurface(self.surface.id);
Expand Down Expand Up @@ -269,6 +314,9 @@ pub const Session = struct {
const ts = std.Io.Clock.now(.awake, self.io);
self.owner.scheduler.tick(ts.nanoseconds);
self.owner.flushDirty(&bctx);
// A rebuild can add or remove focusable nodes, so the traversal order is
// rebuilt from the tree rather than kept incrementally.
try self.focus_mgr.collect(self.gpa, self.el);

const rt = try self.ctx.renderTarget(self.surface.id);
const vp = phantom.PhysicalSize{
Expand Down Expand Up @@ -347,13 +395,80 @@ pub const Session = struct {
@as(f32, @floatCast(a.horizontal)) * self.scale,
@as(f32, @floatCast(a.vertical)) * self.scale,
),
.key => |k| dispatchKey(&self.focus_mgr, k),
else => {},
},
else => {},
}
}
};

test "a lattice key event becomes a phantom key event" {
if (!compositor_builds) return;
const lk = lattice.event.KeyEvent{
.keycode = 30,
.state = .pressed,
.keysym = 'a',
.mods = .{ .shift = true },
.text = "A",
};
const ev = keyEventFromLattice(lk);
try std.testing.expectEqual(phantom.input.Keysym.fromCodepoint('a'), ev.keysym);
try std.testing.expectEqualStrings("A", ev.text.?);
try std.testing.expect(ev.mods.shift);
try std.testing.expect(!ev.mods.ctrl);
try std.testing.expectEqual(phantom.input.KeyAction.press, ev.action);
}

test "a released lattice key reports the release action and keeps its keysym" {
if (!compositor_builds) return;
const lk = lattice.event.KeyEvent{ .keycode = 30, .state = .released, .keysym = 'a' };
const ev = keyEventFromLattice(lk);
try std.testing.expectEqual(phantom.input.KeyAction.release, ev.action);
try std.testing.expectEqual(phantom.input.Keysym.fromCodepoint('a'), ev.keysym);
try std.testing.expect(ev.text == null);
try std.testing.expect(ev.mods.none());
}

test "a key event reaches the focused node's handler" {
if (!compositor_builds) return;
const Seen = struct {
var last: ?phantom.input.Keysym = null;
fn onKey(_: *anyopaque, ev: phantom.input.KeyEvent) bool {
last = ev.keysym;
return true;
}
};
Seen.last = null;

var dummy: u8 = 0;
var handlers = phantom.FocusHandlers{ .ctx = &dummy, .on_key = Seen.onKey };
var mgr = phantom.FocusManager{};
defer mgr.deinit(std.testing.allocator);
try mgr.order.append(std.testing.allocator, &handlers);
mgr.focusNext();

dispatchKey(&mgr, .{ .keycode = 30, .state = .pressed, .keysym = 'a' });
try std.testing.expectEqual(@as(?phantom.input.Keysym, phantom.input.Keysym.fromCodepoint('a')), Seen.last);
}

test "Tab moves the focus through the window path" {
if (!compositor_builds) return;
var dummy: u8 = 0;
var first = phantom.FocusHandlers{ .ctx = &dummy };
var second = phantom.FocusHandlers{ .ctx = &dummy };
var mgr = phantom.FocusManager{};
defer mgr.deinit(std.testing.allocator);
try mgr.order.append(std.testing.allocator, &first);
try mgr.order.append(std.testing.allocator, &second);
mgr.focusNext();

// 0xFF09 is the X11 Tab keysym, which is what lattice reports after its
// keymap resolves the key.
dispatchKey(&mgr, .{ .keycode = 15, .state = .pressed, .keysym = 0xFF09 });
try std.testing.expect(mgr.current == &second);
}

test "the default options open the window phantom has always opened" {
const o = Options{};
try std.testing.expectEqualStrings("phantom", o.title);
Expand Down