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
5 changes: 5 additions & 0 deletions app/layout/Picker.zig
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ is_open: bool = false,
region: []const u8 = "",
/// Where to put the popup; centred on the window when null.
anchor: ?dvui.Point.Natural = null,
/// The control it was opened from, which it grows out of (`core.widgets.popup`'s `grow_from`): the
/// corner's button turning into the picker. Set after `open` by whoever opened it.
grow_from: ?dvui.Rect.Natural = null,
/// A card was dragged out of the popup and is riding the pointer as a loose
/// `ViewDrag`. The popup is closed; `draw` drives the drag instead until the
/// button comes up.
Expand All @@ -67,6 +70,7 @@ pub fn open(self: *Picker, gpa: std.mem.Allocator, region: []const u8, anchor: ?
self.close(gpa);
self.region = gpa.dupe(u8, region) catch return;
self.anchor = anchor;
self.grow_from = null;
self.is_open = true;
}

Expand Down Expand Up @@ -117,6 +121,7 @@ pub fn draw(self: *Picker, f: *Layout) void {
.open_flag = &self.is_open,
.from = self.anchor,
.frost = core.widgets.menuFrost(),
.grow_from = self.grow_from,
}, core.widgets.menuSurfaceOptions().override(.{
.padding = dvui.Rect.all(8),
})) orelse {
Expand Down
417 changes: 288 additions & 129 deletions app/layout/Region.zig

Large diffs are not rendered by default.

51 changes: 48 additions & 3 deletions core/widgets/menu/FloatingMenu.zig
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ pub const InitOptions = struct {
/// Blur what is behind the panel, as a dialog does. The tint *is* the fill, so a frosted
/// menu paints no background of its own. Null draws `opts.color_fill` the ordinary way.
frost: ?BlurBackdrop.Pane = null,
/// The control it grows out of (`reveal`): it opens as that rect and its round corners, and
/// they become its own — the button turning into the menu rather than opening one — its rows
/// coming in once there is room for them. Null slides open from the edge nearest `from`.
grow_from: ?Rect.Natural = null,
};

render_ftb: dvui.RenderFrontToBack,
Expand All @@ -108,6 +112,11 @@ prevClip: Rect.Physical,
scale_val: f32,
/// Sliding open this frame (`reveal`), so its scroll area shows no bar.
revealing: bool = false,
/// Growing out of `InitOptions.grow_from` this frame: how far, 0…1 (past 1 in the swing), and
/// the corner it started from, natural. Null once it is whole.
grown: ?struct { k: f32, r0: f32 } = null,
/// The alpha its rows were drawn at while it grows, to put back (`deinit`).
prev_alpha: ?f32 = null,
menu: Menu,
style: Style,
scaler: dvui.ScaleWidget,
Expand Down Expand Up @@ -172,13 +181,25 @@ pub fn init(self: *FloatingMenu, src: std.builtin.SourceLocation, init_opts: Ini
self.wd.rect = .cast(dvui.placeOnScreen(screens.menuScreenFor(centering).insetAll(screen_margin), .{}, .none, .cast(self.data().rect)));
}

if (init_opts.grow_from) |g| {
// Growing out of a control, it opens in place: sharing the control's corner nearest the
// middle of its screen's far side — the top right of a place's corner button — so it
// grows out of the control the way it is going, never toward a spot beside it.
const scr = screens.menuScreenFor(g).insetAll(screen_margin);
const right = g.x + g.w / 2 > scr.x + scr.w / 2;
const bottom = g.y + g.h / 2 > scr.y + scr.h / 2;
self.wd.rect.x = if (right) g.x + g.w - self.wd.rect.w else g.x;
self.wd.rect.y = if (bottom) g.y + g.h - self.wd.rect.h else g.y;
self.wd.rect = .cast(dvui.placeOnScreen(scr, .{}, .none, .cast(self.data().rect)));
}

if (dvui.snapToPixels()) {
const s = dvui.windowNaturalScale();
self.wd.rect.x = @round(self.wd.rect.x * s) / s;
self.wd.rect.y = @round(self.wd.rect.y * s) / s;
}

self.reveal(init_opts.from);
self.reveal(init_opts.from, init_opts.grow_from);

self.data().register();
dvui.parentSet(self.widget());
Expand Down Expand Up @@ -234,7 +255,9 @@ pub fn init(self: *FloatingMenu, src: std.builtin.SourceLocation, init_opts: Ini
// and `WidgetData.init` is what normally resolves it. An unresolved corner draws square
// whatever radius it names — which is exactly how a rounded palette ended up beside a
// square menu built from the same constant.
const corners = options.cornersGet().finalize(options.themeGet());
var corners = options.cornersGet().finalize(options.themeGet());
// Growing out of a control: its round corners carried into the menu's.
if (self.grown) |g| corners = .round(std.math.lerp(g.r0, corners.tl.radius(), std.math.clamp(g.k, 0, 1)));
BlurBackdrop.frostPane(self.data().id, brs.r, corners, brs.s, frost);
// The shadow as a ring round the glass, after it, so the glass never blurs it in
// (`core.dialogs.glassShadow`).
Expand All @@ -249,6 +272,13 @@ pub fn init(self: *FloatingMenu, src: std.builtin.SourceLocation, init_opts: Ini
scroll_opts.box_shadow = null;
}
if (native) scroll_opts.background = false;
// Growing out of a control: its rows come in over the second half, once there is room for
// them, so the button reads as becoming the menu rather than as a menu squeezed small.
if (self.grown) |g| {
const prev = dvui.currentWindow().alpha;
self.prev_alpha = prev;
dvui.alphaSet(prev * std.math.clamp((g.k - 0.5) / 0.5, 0, 1));
}
// No scrollbar while it slides open: shorter than its rows for those frames, it would show
// one for a list that fits. A menu too long for the window still gets one once it is open.
self.scroll.init(@src(), .{ .horizontal = .none, .vertical_bar = if (self.revealing) .hide else .auto }, scroll_opts);
Expand All @@ -265,7 +295,10 @@ pub fn init(self: *FloatingMenu, src: std.builtin.SourceLocation, init_opts: Ini
/// playful — as a dialog and the palette open. It grows from the edge nearest where it was
/// opened: down from a menu placed below its anchor, up from one placed above it. Only once its
/// size is known (its second frame); a menu that comes back slides open anew.
fn reveal(self: *FloatingMenu, from: ?Rect.Natural) void {
///
/// Given `grow_from`, it grows out of that control instead: its rect carried from the control's
/// to its own on the same curve, and its corners with it (`grown`).
fn reveal(self: *FloatingMenu, from: ?Rect.Natural, grow_from: ?Rect.Natural) void {
const id = self.data().id;
if (dvui.minSizeGet(id) == null) return;
const full = self.wd.rect;
Expand All @@ -282,6 +315,17 @@ fn reveal(self: *FloatingMenu, from: ?Rect.Natural) void {
const a = dvui.animationGet(id, "_reveal") orelse return;
self.revealing = true;
const k = @max(0, a.value());
if (grow_from) |g| {
self.wd.rect = .{
.x = std.math.lerp(g.x, full.x, k),
.y = std.math.lerp(g.y, full.y, k),
.w = @max(1, std.math.lerp(g.w, full.w, k)),
.h = @max(1, std.math.lerp(g.h, full.h, k)),
};
self.grown = .{ .k = k, .r0 = @min(g.w, g.h) / 2 };
dvui.refresh(null, @src(), id);
return;
}
self.wd.rect.h = full.h * k;
// Placed above what opened it: grow up, from its bottom edge.
const above = if (from) |fr| full.y + full.h <= fr.y + 1 else false;
Expand Down Expand Up @@ -349,6 +393,7 @@ pub fn chainFocused(self: *FloatingMenu, self_call: bool) bool {
pub fn deinit(self: *FloatingMenu) void {
defer if (dvui.widgetIsAllocated(self)) dvui.widgetFree(self);
defer self.* = undefined;
if (self.prev_alpha) |a| dvui.alphaSet(a);

const evts = dvui.events();
const rs = self.data().rectScale();
Expand Down
3 changes: 3 additions & 0 deletions core/widgets/menu/Popup.zig
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub const InitOptions = struct {
from: ?dvui.Point.Natural = null,
/// Blur what is behind it, as a dialog does (`FloatingMenu.InitOptions.frost`).
frost: ?BlurBackdrop.Pane = null,
/// The control it grows out of (`FloatingMenu.InitOptions.grow_from`).
grow_from: ?dvui.Rect.Natural = null,
};

init_opts: InitOptions,
Expand Down Expand Up @@ -46,6 +48,7 @@ pub fn active(self: *Popup, src: std.builtin.SourceLocation, init_opts: InitOpti
.style = .popup,
.from = if (init_opts.from) |f| .fromPoint(f) else null,
.frost = init_opts.frost,
.grow_from = init_opts.grow_from,
}, opts);

return self;
Expand Down
1 change: 1 addition & 0 deletions docs/AUTOMATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ user's motion settings, timed in demo time.
| `fizzy.window:main` | the main window |
| `fizzy.window:dialog:<title>`, `fizzy.window:float:<title>`, `fizzy.window:menu` | an OS window of the app's own: a dialog's, a popped-out view's, a menu's (a second of the same name gets `:2`) |
| `region:<name>` | a layout region (`Main`, `Panel`, …) |
| `region.arrange:<name>` | a region's arrange button, where it grows in the region's corner (move to the corner first to bring it up) |
| `float-header:<place>`, `float-close:<place>` | a float's header, to carry it by, and its close button: `float-close:Float 1` |
| `picker:<view id>` | a view's card in a place's picker |
| `picker.mode:one`, `picker.mode:many` | the picker's Single / Multiple |
Expand Down
14 changes: 7 additions & 7 deletions src/editor/demos/catalog.zig
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,18 @@ pub fn place(s: *Script, name: []const u8) ![]const u8 {
return s.print("region:{s}", .{name});
}

/// Where a place's corner button is, `inset` points in from the top-right of the place's region:
/// 26 for the sidebar, 34 for a card with padding round its contents (Main, the panel). The button
/// shows only while the pointer is on the corner, so `cornerButton` comes at it from the corner.
pub fn corner(s: *Script, name: []const u8, inset: f32) !Tape.Target {
return .{ .tag = try place(s, name), .x = 1, .y = 0, .dx = -inset, .dy = inset };
/// Where a place's corner button is: its anchor (`region.arrange:<name>`), wherever it grows in the
/// place's corner. The button shows only while the pointer is on the corner, so `cornerButton`
/// comes at it from the corner.
pub fn corner(s: *Script, name: []const u8) !Tape.Target {
return .{ .tag = try s.print("region.arrange:{s}", .{name}) };
}

/// Bring a place's corner button up and rest on it: from the very corner, where it starts to
/// show, then in onto it.
pub fn cornerButton(s: *Script, name: []const u8, inset: f32) !void {
pub fn cornerButton(s: *Script, name: []const u8) !void {
try s.moveTo(.{ .tag = try place(s, name), .x = 1, .y = 0, .dx = -3, .dy = 3 }, .{ .ms = 500 });
try s.moveTo(try corner(s, name, inset), .{ .ms = 200 });
try s.moveTo(try corner(s, name), .{ .ms = 200 });
}

/// The grip at the foot of `rel`'s pane, shown once it has history: holding on it grows the
Expand Down
8 changes: 4 additions & 4 deletions src/editor/demos/tour.zig
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ pub fn build(s: *Script) !void {
try s.chapter("Places");
try scene(s, .reset);
try s.caption("Every place has a corner. Its button chooses what the place shows…", .{});
try catalog.cornerButton(s, "Sidebar", 26);
try s.click(try catalog.corner(s, "Sidebar", 26), .{ .move_ms = 0 });
try catalog.cornerButton(s, "Sidebar");
try s.click(try catalog.corner(s, "Sidebar"), .{ .move_ms = 0 });
try s.waitFor("picker.action:split", .{});
s.pause(1200);
try s.caption("…and splits it, for two views side by side.", .{ .near = "picker.action:split" });
Expand All @@ -87,8 +87,8 @@ pub fn build(s: *Script) !void {
try s.chapter("Floats");
try scene(s, .reset);
try s.caption("Carry a view from its corner and drop it on the middle of its own place: it floats.", .{});
try catalog.cornerButton(s, "Sidebar", 26);
try s.drag(try catalog.corner(s, "Sidebar", 26), .{ .tag = try catalog.place(s, "Sidebar") }, .{ .ms = 900 });
try catalog.cornerButton(s, "Sidebar");
try s.drag(try catalog.corner(s, "Sidebar"), .{ .tag = try catalog.place(s, "Sidebar") }, .{ .ms = 900 });
try s.waitFor(try catalog.place(s, "Float 1"), .{});
try s.caption("A float goes wherever you carry it by its header.", .{ .near = "float-header:Float 1" });
try s.drag(.{ .tag = "float-header:Float 1" }, .{ .tag = catalog.documents, .x = 0.45, .y = 0.15 }, .{ .ms = 1000 });
Expand Down
9 changes: 5 additions & 4 deletions tests/tapes/soak.zon
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@

// The explorer out of its place by the place's corner button, dropped on the place's own
// middle: it floats. The pointer touches the corner first, which brings the button up, then
// goes onto the button. Back by resetting the layout, which closes the float and its window.
// goes onto the button (`region.arrange`, where it grows). Back by resetting the layout, which
// closes the float and its window.
.{ .at = 1500, .ms = 250, .do = .{ .move = .{ .tag = "region:Sidebar", .x = 1, .y = 0, .dx = -3, .dy = 3 } } },
.{ .at = 1800, .ms = 150, .do = .{ .move = .{ .tag = "region:Sidebar", .x = 1, .y = 0, .dx = -26, .dy = 26 } } },
.{ .at = 1800, .ms = 150, .do = .{ .move = .{ .tag = "region.arrange:Sidebar" } } },
.{ .at = 2000, .do = .{ .press = .left } },
.{ .at = 2100, .ms = 600, .do = .{ .move = .{ .tag = "region:Sidebar", .x = 0.5, .y = 0.5 } } },
.{ .at = 2900, .do = .{ .release = .left } },
Expand All @@ -47,7 +48,7 @@
.{ .at = 4500, .do = .{ .wait = .{ .until = .{ .shown = "region:Sidebar" }, .timeout = 5000 } } },

.{ .at = 5500, .ms = 250, .do = .{ .move = .{ .tag = "region:Sidebar", .x = 1, .y = 0, .dx = -3, .dy = 3 } } },
.{ .at = 5800, .ms = 150, .do = .{ .move = .{ .tag = "region:Sidebar", .x = 1, .y = 0, .dx = -26, .dy = 26 } } },
.{ .at = 5800, .ms = 150, .do = .{ .move = .{ .tag = "region.arrange:Sidebar" } } },
.{ .at = 6000, .do = .{ .press = .left } },
.{ .at = 6100, .ms = 600, .do = .{ .move = .{ .tag = "region:Sidebar", .x = 0.5, .y = 0.5 } } },
.{ .at = 6900, .do = .{ .release = .left } },
Expand All @@ -57,7 +58,7 @@
.{ .at = 8500, .do = .{ .wait = .{ .until = .{ .shown = "region:Sidebar" }, .timeout = 5000 } } },

.{ .at = 9500, .ms = 250, .do = .{ .move = .{ .tag = "region:Sidebar", .x = 1, .y = 0, .dx = -3, .dy = 3 } } },
.{ .at = 9800, .ms = 150, .do = .{ .move = .{ .tag = "region:Sidebar", .x = 1, .y = 0, .dx = -26, .dy = 26 } } },
.{ .at = 9800, .ms = 150, .do = .{ .move = .{ .tag = "region.arrange:Sidebar" } } },
.{ .at = 10000, .do = .{ .press = .left } },
.{ .at = 10100, .ms = 600, .do = .{ .move = .{ .tag = "region:Sidebar", .x = 0.5, .y = 0.5 } } },
.{ .at = 10900, .do = .{ .release = .left } },
Expand Down
Loading