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
58 changes: 56 additions & 2 deletions core/gfx/LiquidField.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down
31 changes: 31 additions & 0 deletions core/gfx/glass_look.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down
18 changes: 17 additions & 1 deletion core/native_glass.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion sdk/sdk_version.zig
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ const std = @import("std");
pub const sdk_version = std.SemanticVersion{
.major = 0,
.minor = 2,
.patch = 17,
.patch = 18,
};
8 changes: 7 additions & 1 deletion src/editor/Popout.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions tests/integration.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading