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
77 changes: 62 additions & 15 deletions src/dvui.zig
Original file line number Diff line number Diff line change
Expand Up @@ -126,19 +126,27 @@ pub fn defaultDialogCallAfter(id: dvui.Id, response: dvui.enums.DialogResponse)
}
}

/// True when the canvas should not hide the OS cursor or draw tool cursors, and should not
/// treat the pointer as hovering the artboard.
/// - Modal dialogs: block the entire main pane.
/// True when the main workspace canvas should not hide the OS cursor, draw tool cursors, or
/// consume pointer events.
/// - Modal dialogs: always block the editor canvas (not in-dialog previews).
/// - Non-modal floating windows (e.g. Export): block only while the cursor is over that window.
pub fn canvasPointerInputSuppressed() bool {
const cw = dvui.currentWindow();
const main_id = cw.data().id;
var i = cw.subwindows.stack.items.len;
while (i > 1) : (i -= 1) {
if (cw.subwindows.stack.items[i - 1].modal) return true;
for (cw.subwindows.stack.items[1..]) |sub| {
if (sub.modal) return true;
}
const target = cw.subwindows.windowFor(cw.mouse_pt);
return target != main_id and target != .zero;
return target != .zero and target != main_id;
}

/// In-dialog preview canvases (Grid Layout): allow pan/zoom while the pointer is over the
/// dialog subwindow that owns the preview.
pub fn dialogCanvasPointerInputSuppressed() bool {
const cw = dvui.currentWindow();
const sub = cw.subwindows.current() orelse return true;
const target = cw.subwindows.windowFor(cw.mouse_pt);
return target != sub.id;
}

/// Creates a new file dialog with necessary data set and returns the id mutex.
Expand Down Expand Up @@ -950,6 +958,13 @@ pub const SpriteInitOptions = struct {
depth: f32 = 0.0, // -1.0 is front, 1.0 is back
reflection: bool = false,
overlap: f32 = 0.0,
/// Overall opacity in [0, 1]; 1.0 is fully opaque. Used to fade cards out
/// toward the background the further they sit from the focus.
opacity: f32 = 1.0,
/// Vertical shift (logical px, positive = down) applied to the reflection
/// only. Lets the reflection slide away from the card — e.g. as a card flies
/// up out of view, its reflection sinks down, like peeling off a waterline.
reflection_offset: f32 = 0.0,
};

pub fn sprite(src: std.builtin.SourceLocation, init_opts: SpriteInitOptions, opts: dvui.Options) dvui.WidgetData {
Expand Down Expand Up @@ -1037,24 +1052,38 @@ pub fn sprite(src: std.builtin.SourceLocation, init_opts: SpriteInitOptions, opt
path.addPoint(bottom_right);
path.addPoint(bottom_left);

// Distance fade toward transparent: `fade_white` tints textured draws by the
// card opacity, and `op` scales the alpha of solid fills. No-ops at op == 1.
const op = std.math.clamp(init_opts.opacity, 0.0, 1.0);
const fade_white = dvui.Color.white.opacity(op);

if (init_opts.reflection) {
var path2: dvui.Path.Builder = .init(dvui.currentWindow().arena());
defer path2.deinit();

path2.addPoint(bottom_left.plus(.{ .x = -(top_right.x - top_left.x) * 0.5, .y = (bottom_left.y - top_left.y) * 0.75 }));
path2.addPoint(bottom_right.plus(.{ .x = (bottom_right.x - bottom_left.x) * 0.5, .y = (bottom_left.y - top_left.y) * 0.75 }));
path2.addPoint(bottom_right);
path2.addPoint(bottom_left);
// Direct vertical mirror: reflect each (already skewed) top corner straight
// down through its bottom corner, so the reflection is a true flip of the
// card — same width and skew at every height, sharing the bottom edge —
// rather than a trapezoid that flares outward. pathToSubdividedQuad reads
// these as (tl, tr, br, bl); the far edge (tl, tr) samples the sprite top
// and the near edge (br, bl) the sprite bottom, giving the mirrored uv.
// `refl_off` slides the whole reflection down independently of the card.
const refl_off = dvui.Point.Physical{ .x = 0.0, .y = init_opts.reflection_offset * wd.contentRectScale().s };
path2.addPoint(bottom_left.plus(bottom_left.diff(top_left)).plus(refl_off));
path2.addPoint(bottom_right.plus(bottom_right.diff(top_right)).plus(refl_off));
path2.addPoint(bottom_right.plus(refl_off));
path2.addPoint(bottom_left.plus(refl_off));

const preview_extent = @min(wd.contentRectScale().r.w, wd.contentRectScale().r.h);
const subdivisions_f = std.math.clamp(preview_extent / 96.0, 2.0, 6.0);
const subdivisions: usize = @intFromFloat(subdivisions_f);

if (init_opts.alpha_source) |alpha_source| {
const reflection_path = path2.build();

var reflection_triangles_bg = pathToSubdividedQuad(reflection_path, dvui.currentWindow().arena(), .{
.subdivisions = subdivisions,
.color_mod = dvui.themeGet().color(.content, .fill).lighten(4.0),
.color_mod = dvui.themeGet().color(.content, .fill).lighten(4.0).opacity(op),
.vertical_fade = true,
}) catch unreachable;
defer reflection_triangles_bg.deinit(dvui.currentWindow().arena());
Expand All @@ -1063,7 +1092,7 @@ pub fn sprite(src: std.builtin.SourceLocation, init_opts: SpriteInitOptions, opt
.subdivisions = subdivisions,
.uv = uv,
.vertical_fade = true,
.color_mod = dvui.Color.white,
.color_mod = fade_white,
}) catch unreachable;
defer reflection_triangles_layers.deinit(dvui.currentWindow().arena());

Expand Down Expand Up @@ -1117,11 +1146,24 @@ pub fn sprite(src: std.builtin.SourceLocation, init_opts: SpriteInitOptions, opt
}

if (init_opts.alpha_source) |alpha_source| {
wd.contentRectScale().r.fill(.all(0), .{ .color = dvui.themeGet().color(.content, .fill), .fade = 1.5 });
if (init_opts.depth != 0.0) {
// Skew the opaque base along with the art so no axis-aligned sliver
// of fill colour pokes out past the receding edge.
var base_triangles = pathToSubdividedQuad(path.build(), dvui.currentWindow().arena(), .{
.subdivisions = 8,
.color_mod = dvui.themeGet().color(.content, .fill).opacity(op),
}) catch unreachable;
defer base_triangles.deinit(dvui.currentWindow().arena());
dvui.renderTriangles(base_triangles, null) catch {
dvui.log.err("Failed to render triangles", .{});
};
} else {
wd.contentRectScale().r.fill(.all(0), .{ .color = dvui.themeGet().color(.content, .fill).opacity(op), .fade = 1.5 });
}

const alpha_triangles = pathToSubdividedQuad(path.build(), dvui.currentWindow().arena(), .{
.subdivisions = 8,
.color_mod = dvui.themeGet().color(.content, .fill).lighten(6.0).opacity(0.5),
.color_mod = dvui.themeGet().color(.content, .fill).lighten(6.0).opacity(0.5).opacity(op),
}) catch unreachable;
dvui.renderTriangles(alpha_triangles, alpha_source.getTexture() catch null) catch {
dvui.log.err("Failed to render triangles", .{});
Expand All @@ -1137,13 +1179,18 @@ pub fn sprite(src: std.builtin.SourceLocation, init_opts: SpriteInitOptions, opt
},
.uv = uv,
.corner_radius = .all(0),
.color_mod = fade_white,
// When skewed, render the layer stack into the same quad as the
// background so the art tilts like a record on a shelf.
.quad = if (init_opts.depth != 0.0) .{ top_left, top_right, bottom_right, bottom_left } else null,
}) catch {
dvui.log.err("Failed to render layers", .{});
};
} else {
const triangles = pathToSubdividedQuad(path.build(), dvui.currentWindow().arena(), .{
.subdivisions = 8,
.uv = uv,
.color_mod = fade_white,
}) catch unreachable;

dvui.renderTriangles(triangles, init_opts.source.getTexture() catch null) catch {
Expand Down
4 changes: 4 additions & 0 deletions src/editor/Settings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ hold_menu_duration_ms: u32 = 500,
/// Whether or not to show rulers on each canvas.
show_rulers: bool = true,

/// Sprites panel: fly side cards away for a single-card focus view and snap
/// scroll when the focus sprite changes (also toggled from the sprites pane).
scrolling_cards: bool = false,

/// When true, print frame/draw perf stats to the console (Debug / ReleaseSafe only for tick stats).
perf_logging: bool = false,

Expand Down
53 changes: 33 additions & 20 deletions src/editor/dialogs/GridLayout.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const std = @import("std");

const NewFile = @import("NewFile.zig");
const CanvasWidget = @import("../widgets/CanvasWidget.zig");
const FloatingWindowWidget = @import("../widgets/FloatingWindowWidget.zig");
const builtin = @import("builtin");

/// Editable grid fields for one mode (Slice vs Resize each keep their own backing).
Expand Down Expand Up @@ -107,7 +108,7 @@ pub fn presetFromFile(file: *fizzy.Internal.File) void {
// `prev_size` matches `data_size` and `second_center` is false, so `install` skips the
// rescale/recenter pass and the preview ends up offscreen / at a stale zoom. Resetting to
// a fresh widget forces a fit-to-pane on the next frame.
preview_canvas = .{};
preview_canvas = .{ .pointer_scope = .dialog };
left_scroll = .{ .horizontal = .auto };
dialog_middle_scroll = .{ .horizontal = .auto, .vertical = .auto };
preview_pane_fit_w = 0;
Expand Down Expand Up @@ -551,32 +552,30 @@ fn renderPreview(

const dims_changed = nw != preview_last_nw or nh != preview_last_nh;

const shell_drag_or_resize = blk: {
const shell_resize_drag = blk: {
const wid = dvui.dataGet(null, mutex_id, "_grid_layout_float_wd_id", dvui.Id) orelse break :blk false;
break :blk dvui.captured(wid);
break :blk FloatingWindowWidget.DragPart.isResizeDrag(wid);
};

const host_vp_versus_stored = host_vp_ok and (preview_viewport_fit_w < 4 or preview_viewport_fit_h < 4 or
@abs(vp_host_w - preview_viewport_fit_w) >= preview_layout_min_delta or
@abs(vp_host_h - preview_viewport_fit_h) >= preview_layout_min_delta);
const needs_preinstall_refit = host_vp_ok and (fit_key_changed or dims_changed or host_vp_versus_stored or shell_drag_or_resize);
const host_changed = host_vp_ok and (preview_pane_fit_w < 4 or preview_pane_fit_h < 4 or
@abs(vp_host_w - preview_pane_fit_w) >= preview_layout_min_delta or
@abs(vp_host_h - preview_pane_fit_h) >= preview_layout_min_delta);
const needs_preinstall_refit = host_vp_ok and (fit_key_changed or dims_changed or host_changed or shell_resize_drag);

const preview_data: dvui.Size = .{ .w = @floatFromInt(nw), .h = @floatFromInt(nh) };

const reset_viewport_after_fit = fit_key_changed or dims_changed or host_changed or shell_resize_drag;

if (needs_preinstall_refit) {
preview_canvas.fitContentContainInHost(
preview_data,
dvui.Rect{ .x = 0, .y = 0, .w = vp_host_w, .h = vp_host_h },
1.2,
);
preview_canvas.scroll_info.viewport.x = 0;
preview_canvas.scroll_info.viewport.y = 0;
// `CanvasWidget.install` restores origin/viewport from this snapshot whenever it sees the
// parent rect moving (e.g. during window resize) — without syncing it here, the scaler is
// placed at the pre-resize origin and the image looks "stuck" until the resize ends.
preview_canvas.stable_origin = preview_canvas.origin;
preview_canvas.stable_viewport = preview_canvas.scroll_info.viewport;
preview_canvas.stable_virtual_size = preview_canvas.scroll_info.virtual_size;
if (reset_viewport_after_fit) {
preview_canvas.scroll_info.viewport.x = 0;
preview_canvas.scroll_info.viewport.y = 0;
}
if (fit_key_changed) {
preview_fit_key_cache = fit_key;
}
Expand Down Expand Up @@ -609,14 +608,17 @@ fn renderPreview(

var did_post_install_refit = false;
const needs_bootstrap_refit = !host_vp_ok and vp_ok and (fit_key_changed or dims_changed);
if (needs_bootstrap_refit) {
const needs_post_install_host_refit = vp_ok and (host_changed or shell_resize_drag);
if (needs_bootstrap_refit or needs_post_install_host_refit) {
preview_canvas.fitContentContainInHost(
preview_data,
dvui.Rect{ .x = 0, .y = 0, .w = vpw, .h = vph },
1.2,
);
preview_canvas.scroll_info.viewport.x = 0;
preview_canvas.scroll_info.viewport.y = 0;
if (reset_viewport_after_fit or needs_post_install_host_refit) {
preview_canvas.scroll_info.viewport.x = 0;
preview_canvas.scroll_info.viewport.y = 0;
}
if (fit_key_changed) {
preview_fit_key_cache = fit_key;
}
Expand All @@ -629,10 +631,21 @@ fn renderPreview(
dvui.refresh(null, @src(), preview_canvas.id);
}

// `CanvasWidget.install` restores this snapshot while the parent rect is mid-resize; keep
// origin and viewport in sync so the preview stays centered, not pinned to the upper-left.
if (needs_preinstall_refit or did_post_install_refit) {
preview_canvas.stable_origin = preview_canvas.origin;
preview_canvas.stable_viewport = preview_canvas.scroll_info.viewport;
preview_canvas.stable_virtual_size = preview_canvas.scroll_info.virtual_size;
preview_canvas.has_stable_snapshot = true;
}

preview_viewport_fit_w = vpw;
preview_viewport_fit_h = vph;
preview_pane_fit_w = vpw;
preview_pane_fit_h = vph;
if (host_vp_ok) {
preview_pane_fit_w = vp_host_w;
preview_pane_fit_h = vp_host_h;
}

const any_refit = needs_preinstall_refit or did_post_install_refit;

Expand Down
6 changes: 6 additions & 0 deletions src/editor/explorer/settings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@ pub fn draw() !void {
})) {
fizzy.editor.markSettingsDirty();
}

if (dvui.checkbox(@src(), &fizzy.editor.settings.scrolling_cards, "Scrolling sprite cards", .{
.expand = .none,
})) {
fizzy.editor.markSettingsDirty();
}
}

{
Expand Down
Loading
Loading