diff --git a/core/gfx/LiquidField.zig b/core/gfx/LiquidField.zig index d12c302e..89631dc9 100644 --- a/core/gfx/LiquidField.zig +++ b/core/gfx/LiquidField.zig @@ -20,7 +20,8 @@ //! //! **Drawing.** Inside a layer's `dvui.deferRender` job, after its capture: `draw` with the //! frost and what it covers. False where the backend has no programs (`core.gfx.programs`) or the -//! program is not ready yet, and the caller draws its meshes instead, without the joins. +//! program is not ready yet, and the caller draws its meshes instead, without the joins. A lens +//! over a picture of the caller's own (a magnifier) needs no capture: `drawPicture` queues itself. //! //! Layers do not join each other: a dialog over a drop is two layers, the dialog's capture taken //! with the drop already on the frame. @@ -75,6 +76,9 @@ refraction: f32 = 1, /// It carries text — a dialog, a menu — and keeps some frost over a clear lens to read /// (`glass_look.forText`). text: bool = false, +/// It is a lens over a picture of the caller's own (`drawPicture`): its middle the picture as it +/// is, only the band along its edge glass (`glass_look.forLens`). +lens: bool = false, pub fn add(self: *LiquidField, shape: Shape) void { if (self.len >= max_shapes) return; @@ -325,7 +329,7 @@ pub fn draw(self: *const LiquidField, frost: dvui.Texture, covered: dvui.Rect.Ph // The window behind the glass, published for this frame. u.backdrop[0] = if (opaqueWindow()) 1 else 0; // And the glass the slider makes of it this frame (`publishLook`). - if (publishedLook()) |l| self.applyLook(&u, if (self.text) glass_look.forText(l) else glass_look.forDrops(l)); + if (publishedLook()) |l| self.applyLook(&u, if (self.lens) glass_look.forLens(l) else if (self.text) glass_look.forText(l) else glass_look.forDrops(l)); const textures = [_]?*anyopaque{programs.handle(sharp)}; if (!h.begin(id, &textures, textures.len, u.vec4s(), uniform_vec4s)) return false; defer h.end(); @@ -336,6 +340,56 @@ pub fn draw(self: *const LiquidField, frost: dvui.Texture, covered: dvui.Rect.Ph return true; } +/// The shapes as a lens over a picture of the caller's own — a magnifier's zoom, a loupe — rather +/// than over what is behind them. `picture` is a picture of `covered` (physical pixels, window +/// coordinates), reaching `pictureMargin` past the shapes. Its middle is the picture as it is, +/// unblurred, untinted and unlit, so a colour picked through it is the colour under it; only the +/// band along its edge is glass (`lens`, `glass_look.forLens`). Where the app publishes no look +/// (the web), it is the earlier glass, its rim pulling from just outside the shape, which is what +/// the margin is for. Nothing under it is read, so it costs no capture. +/// +/// Queued in order with the frame (`dvui.deferRender`), as every glass is, and drawn at full +/// alpha: glass replaces what it covers, and at part alpha it left a hole. `picture` must live +/// until the frame ends. False, having queued nothing, where there is no glass program (`ready`): +/// draw the picture some other way. +pub fn drawPicture(self: *const LiquidField, picture: dvui.Texture, covered: dvui.Rect.Physical) bool { + if (self.len == 0) return true; + if (!ready()) return false; + const job = dvui.currentWindow().arena().create(PictureJob) catch return false; + job.* = .{ .field = self.*, .picture = picture, .covered = covered }; + job.field.lens = true; + job.field.tint = null; + job.field.mix = 0; + job.field.lift = 0; + for (job.field.shapes[0..job.field.len]) |*sh| sh.blur = 0; + dvui.deferRender(job, PictureJob.run); + return true; +} + +/// Physical pixels the picture `drawPicture` lays the glass over should reach past the shapes: as +/// far as the earlier glass's rim reaches out (`liquid_glass.margin`), at the strongest of its +/// shapes' lenses. Apple's lens pulls only from inside the shape and needs none of it. +pub fn pictureMargin(self: *const LiquidField) f32 { + var lens: f32 = 0; + for (self.shapes[0..self.len]) |sh| lens = @max(lens, sh.lens); + return liquid_glass.margin(.{ .lens = lens, .refraction = self.refraction }, self.scale); +} + +/// What `drawPicture` hands to the replay, in the frame's arena. +const PictureJob = struct { + field: LiquidField, + picture: dvui.Texture, + covered: dvui.Rect.Physical, + + fn run(ctx: ?*anyopaque) void { + const self: *PictureJob = @ptrCast(@alignCast(ctx orelse return)); + const prev_alpha = dvui.currentWindow().alpha; + dvui.alphaSet(1); + defer dvui.alphaSet(prev_alpha); + _ = self.field.draw(self.picture, self.covered, null); + } +}; + /// One quad per group, its vertex colour telling the program which pass and which shapes. fn quads(self: *const LiquidField, groups: Clusters, glass: bool, frost: dvui.Texture) void { const arena = dvui.currentWindow().arena(); diff --git a/core/gfx/glass_look.zig b/core/gfx/glass_look.zig index 22b6bf8d..e746dd56 100644 --- a/core/gfx/glass_look.zig +++ b/core/gfx/glass_look.zig @@ -261,6 +261,21 @@ pub fn forText(look: InApp) InApp { return l; } +/// `look` for a lens over a picture of the caller's own — a magnifier's zoom, a loupe — rather than +/// over what is behind it (`LiquidField.drawPicture`): the picture is what it shows, as it is, so +/// its middle takes no frost, none of the window's colour and no lift, at any point on the slider +/// — a colour picked through it is the colour under it. Only the band along its edge is glass, +/// bending the picture toward its rim and lit there, and that follows the slider as every glass +/// does: a clear lens at the bottom, its shine going at the top. +pub fn forLens(look: InApp) InApp { + var l = look; + l.frost = 0; + l.mix = 0; + l.text_mix = 0; + l.lift = 0; + return l; +} + fn smoothstep(x: f32) f32 { return x * x * (3 - 2 * x); } @@ -325,6 +340,22 @@ test "a surface carrying text keeps some frost over a clear lens, none once it i try std.testing.expectEqual(inApp(1).frost, forText(inApp(1)).frost); } +test "a lens over a picture leaves its middle as it is, everywhere on the slider" { + var i: usize = 0; + while (i <= 20) : (i += 1) { + const t = @as(f32, @floatFromInt(i)) / 20; + const l = forLens(inApp(t)); + try std.testing.expectEqual(@as(f32, 0), l.frost); + try std.testing.expectEqual(@as(f32, 0), l.mix); + try std.testing.expectEqual(@as(f32, 0), l.lift); + // Its edge is the slider's glass all the same. + try std.testing.expectEqual(inApp(t).bend, l.bend); + try std.testing.expectEqual(inApp(t).rim, l.rim); + } + // A clear lens at the bottom, folding at its rim as Apple's does. + try std.testing.expect(forLens(inApp(0)).bend > 1); +} + test "a drop's plain blur is light, growing with the glass's roughness" { try std.testing.expectApproxEqAbs(drop_blur_min, native(0).blur, 1e-6); try std.testing.expectApproxEqAbs(drop_blur, native(1).blur, 1e-6); diff --git a/core/native_glass.zig b/core/native_glass.zig index 688b792d..b7905b44 100644 --- a/core/native_glass.zig +++ b/core/native_glass.zig @@ -5,7 +5,8 @@ //! app reconciles the OS's glass with it at the end of the frame; nothing of it is drawn by the app. //! //! In dvui's data, as `screens` is, so whatever draws the glass — fizzy, a plugin's drop zones — -//! declares it the same way, with no SDK between them. +//! declares it the same way, with no SDK between them. Outside a drag, glass a plugin declares +//! shows in the same overlay wherever the OS offers it (`offered`). const std = @import("std"); const dvui = @import("dvui"); @@ -52,6 +53,21 @@ pub fn publishOn(on_: bool) void { current().on = on_; } +/// Whether the OS can draw glass declared outside a view drag this frame (a plugin's own, such as +/// pixi's dropper magnifier), set by the app before anything draws. While it can, glass declared +/// with `add` shows in the app's overlay of the OS's glass whatever else is on, and the layers +/// marked to go over it (`core.screens.markCarried`) are taken into the overlay's picture. The +/// app's own windows draw none of either. Where it cannot, draw the app's own glass. +pub fn publishOffered(on_: bool) void { + dvui.dataSet(null, key_id, "_offered", on_); +} + +/// Whether the OS draws glass declared this frame outside a view drag (`publishOffered`): declare +/// it (`add`) and draw none. +pub fn offered() bool { + return dvui.dataGet(null, key_id, "_offered", bool) orelse false; +} + /// Whether the carried view's picture goes under the OS's glass this frame, in a window of its own /// beneath the drag's (the app's, each frame): the glass then bends it, a lens over what is carried. /// The drag hands it over rather than drawing it over the glass. diff --git a/sdk/sdk_version.zig b/sdk/sdk_version.zig index 541df375..7fe028be 100644 --- a/sdk/sdk_version.zig +++ b/sdk/sdk_version.zig @@ -23,5 +23,5 @@ const std = @import("std"); pub const sdk_version = std.SemanticVersion{ .major = 0, .minor = 2, - .patch = 17, + .patch = 18, }; diff --git a/src/editor/Popout.zig b/src/editor/Popout.zig index e6524e88..ee9cb190 100644 --- a/src/editor/Popout.zig +++ b/src/editor/Popout.zig @@ -173,6 +173,10 @@ pub fn beginFrame(state: *State) void { // is on, and after it while its drops are still running back together (`ViewDrag.last_pending`) // — their going, as their coming, is the OS's glass too. fizzy.core.native_glass.publishOn(nativeGlass() and (state.view_drag.active() or state.view_drag.last_pending_count > 0)); + // And glass declared outside a drag, such as a plugin's (pixi's dropper magnifier): the OS's + // wherever it has it, in the same overlay, which stays while any glass is declared + // (`overlayFrame`). + fizzy.core.native_glass.publishOffered(nativeGlass()); // And the carried view's picture under that glass, in a window of its own (`photoFrame`). fizzy.core.native_glass.publishUnder(nativeGlass() and viewports.carries); fizzy.core.screens.publishBeyond(viewports.carries and state.view_drag.active()); @@ -462,7 +466,9 @@ fn overlaySpacing(s: f32) f32 { /// what is under them. The carried view's photograph lies under its drop's glass (`overlayPhoto`); /// what goes over the glass — the drop's name, the bubbles' icons (`core.screens.markCarried`) — is /// taken into the overlay's picture. Glass and pictures change in one transaction. It goes when the -/// drag does; a float the drop opens grows out of a carry window of its own (`growFrame`). +/// drag does; a float the drop opens grows out of a carry window of its own (`growFrame`). Glass +/// declared outside a drag (`core.native_glass.offered`: pixi's dropper magnifier) keeps it up the +/// same way, with what is marked to go over it. fn overlayFrame(state: *State) void { const d = &state.view_drag; // Kept past the drag while its drops still go, and gone once there is no glass left. diff --git a/tests/integration.zig b/tests/integration.zig index 5c9b8360..1c7ed35d 100644 --- a/tests/integration.zig +++ b/tests/integration.zig @@ -4454,6 +4454,25 @@ test "liquid field: groups are the shapes that touch, each drawn as its own quad try std.testing.expectEqual(@as(f32, 22), u.shapes[1][0][0]); } +test "liquid field: a lens's picture reaches as far past its shapes as the earlier glass's rim pulls from" { + const LF = fizzy.core.LiquidField; + var f: LF = .{ .scale = 2, .refraction = 1 }; + f.add(LF.Shape.circle(.{ .x = 100, .y = 100 }, 60)); + // The earlier glass (the web's) pulls its rim from `liquid_glass.refraction` points out at full + // strength: a picture reaching that far past the shape has something there to show. + try std.testing.expect(f.pictureMargin() >= fizzy.core.liquid_glass.refraction * 2); + // Further at the strongest refraction setting. + var strong = f; + strong.refraction = 2; + try std.testing.expect(strong.pictureMargin() > f.pictureMargin()); + // And further for a stronger lens on its shape. + var bent = f; + bent.shapes[0].lens = 1.5; + try std.testing.expect(bent.pictureMargin() > f.pictureMargin()); + // Nothing in fizzy draws one (pixi's dropper magnifier does): compiled here all the same. + _ = &LF.drawPicture; +} + test "liquid glass: the rings of a pane run the way dvui's paths do" { const r: dvui.Rect.Physical = .{ .x = 0, .y = 0, .w = 200, .h = 100 }; var pts: [4 * 7 + 2 * (8 - 1) + 2 * (4 - 1)]dvui.Point.Physical = undefined;