Files
Zivro/src/ui/canvas_view.zig

244 lines
10 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const std = @import("std");
const dvui = @import("dvui");
const dvui_ext = @import("dvui_ext.zig");
const Canvas = @import("../Canvas.zig");
const Rect_i = @import("../models/basic_models.zig").Rect_i;
const Tool = @import("../Tool.zig");
pub fn canvasView(canvas: *Canvas, content_rect_scale: dvui.RectScale) void {
var textured = dvui_ext.texturedBox(content_rect_scale, dvui.Rect.all(20));
{
var overlay = dvui.overlay(@src(), .{ .expand = .both });
{
var scroll = dvui.scrollArea(
@src(),
.{
.scroll_info = &canvas.scroll,
.vertical_bar = .auto,
.horizontal_bar = .auto,
},
.{ .expand = .both, .background = false },
);
{
drawCanvasContent(canvas, scroll);
handleCanvasZoom(canvas, scroll);
handleCanvasMouse(canvas, scroll);
}
scroll.deinit();
// Тулбар поверх scroll
var toolbar_box = dvui.box(
@src(),
.{ .dir = .horizontal },
.{
.expand = .none,
.background = false,
.gravity_x = 0.0,
.gravity_y = 0.0,
.margin = dvui.Rect{ .x = 8, .y = 8 },
},
);
{
drawToolbar(canvas);
}
// Сохраняем rect тулбара для следующего кадра — в handleCanvasMouse исключаем из него клики
canvas.toolbar_rect_scale = toolbar_box.data().contentRectScale();
toolbar_box.deinit();
dvui.label(@src(), "Canvas", .{}, .{ .gravity_x = 0.5, .gravity_y = 0.0 });
}
overlay.deinit();
}
textured.deinit();
}
fn drawCanvasContent(canvas: *Canvas, scroll: anytype) void {
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
const img_size = canvas.getZoomedImageSize();
const viewport_rect = scroll.data().contentRect();
const scroll_current = dvui.Point{ .x = canvas.scroll.viewport.x, .y = canvas.scroll.viewport.y };
const viewport_px = dvui.Rect{
.x = viewport_rect.x * natural_scale,
.y = viewport_rect.y * natural_scale,
.w = viewport_rect.w * natural_scale,
.h = viewport_rect.h * natural_scale,
};
const scroll_px = dvui.Point{
.x = scroll_current.x * natural_scale,
.y = scroll_current.y * natural_scale,
};
const changed = canvas.updateVisibleImageRect(viewport_px, scroll_px);
if (changed)
canvas.requestRedraw();
canvas.processPendingRedraw() catch |err| {
std.debug.print("processPendingRedraw error: {}\n", .{err});
};
const content_w_px: u32 = img_size.x + img_size.w;
const content_h_px: u32 = img_size.y + img_size.h;
const content_w = @as(f32, @floatFromInt(content_w_px)) / natural_scale;
const content_h = @as(f32, @floatFromInt(content_h_px)) / natural_scale;
var canvas_layer = dvui.overlay(
@src(),
.{ .min_size_content = .{ .w = content_w, .h = content_h }, .background = false },
);
{
if (canvas.texture) |tex| {
const vis = canvas._visible_rect orelse Rect_i{ .x = 0, .y = 0, .w = 0, .h = 0 };
const left = @as(f32, @floatFromInt(img_size.x + vis.x)) / natural_scale;
const top = @as(f32, @floatFromInt(img_size.y + vis.y)) / natural_scale;
_ = dvui.image(
@src(),
.{ .source = .{ .texture = tex } },
.{
.background = false,
.expand = .none,
.gravity_x = 0.0,
.gravity_y = 0.0,
.margin = .{ .x = left, .y = top, .w = canvas.pos.x, .h = canvas.pos.y },
.min_size_content = .{
.w = @as(f32, @floatFromInt(vis.w)) / natural_scale,
.h = @as(f32, @floatFromInt(vis.h)) / natural_scale,
},
.max_size_content = .{
.w = @as(f32, @floatFromInt(vis.w)) / natural_scale,
.h = @as(f32, @floatFromInt(vis.h)) / natural_scale,
},
},
);
}
}
canvas_layer.deinit();
}
fn handleCanvasZoom(canvas: *Canvas, scroll: anytype) void {
const ctrl = dvui.currentWindow().modifiers.control();
if (!ctrl) return;
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
for (dvui.events()) |*e| {
switch (e.evt) {
.mouse => |*mouse| {
const action = mouse.action;
if (dvui.eventMatchSimple(e, scroll.data()) and (action == .wheel_x or action == .wheel_y)) {
switch (action) {
.wheel_y => |y| {
const viewport_pt = scroll.data().contentRectScale().pointFromPhysical(mouse.p);
const content_pt = dvui.Point{
.x = viewport_pt.x + canvas.scroll.viewport.x,
.y = viewport_pt.y + canvas.scroll.viewport.y,
};
const doc_pt = canvas.contentPointToDocument(content_pt, natural_scale);
canvas.addZoom(y / 1000);
canvas.requestRedraw();
// Сдвигаем viewport так, чтобы точка под курсором (даже вне холста) не уезжала
const new_zoom = canvas.getZoom();
const img = canvas.getZoomedImageSize();
const new_content_x = (@as(f32, @floatFromInt(img.x)) + doc_pt.x * new_zoom) / natural_scale;
const new_content_y = (@as(f32, @floatFromInt(img.y)) + doc_pt.y * new_zoom) / natural_scale;
canvas.scroll.viewport.x = new_content_x - viewport_pt.x;
canvas.scroll.viewport.y = new_content_y - viewport_pt.y;
const viewport_rect = scroll.data().contentRect();
const content_w = @as(f32, @floatFromInt(img.x + img.w)) / natural_scale;
const content_h = @as(f32, @floatFromInt(img.y + img.h)) / natural_scale;
const max_x = @max(0, content_w - viewport_rect.w + canvas.pos.x);
const max_y = @max(0, content_h - viewport_rect.h + canvas.pos.y);
canvas.scroll.viewport.x = std.math.clamp(canvas.scroll.viewport.x, 0, max_x);
canvas.scroll.viewport.y = std.math.clamp(canvas.scroll.viewport.y, 0, max_y);
},
else => {},
}
e.handled = true;
}
},
else => {},
}
}
}
fn handleCanvasMouse(canvas: *Canvas, scroll: *dvui.ScrollAreaWidget) void {
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
const scroll_data = scroll.data();
for (dvui.events()) |*e| {
switch (e.evt) {
.mouse => |*mouse| {
if (mouse.action != .press or mouse.button != .left) continue;
if (e.handled) continue;
if (!dvui.eventMatchSimple(e, scroll_data)) continue;
// Не обрабатывать клик, если он попал в область тулбара (rect с предыдущего кадра).
if (canvas.toolbar_rect_scale) |trs| {
const pt = trs.pointFromPhysical(mouse.p);
const r = trs.r;
if (pt.x >= 0 and pt.x * trs.s < r.w and pt.y >= 0 and pt.y * trs.s < r.h) continue;
}
const viewport_pt = scroll_data.contentRectScale().pointFromPhysical(mouse.p);
const content_pt = dvui.Point{
.x = viewport_pt.x + canvas.scroll.viewport.x,
.y = viewport_pt.y + canvas.scroll.viewport.y,
};
const doc_pt = canvas.contentPointToDocument(content_pt, natural_scale);
canvas.cursor_document_point = if (canvas.isContentPointOnDocument(content_pt, natural_scale)) doc_pt else null;
if (canvas.cursor_document_point) |point| {
if (canvas.toolbar.currentDescriptor()) |desc| {
var ctx = Tool.ToolContext{
.canvas = canvas,
.document_point = point,
};
desc.implementation.onCanvasClick(&ctx) catch |err| {
std.debug.print("onCanvasClick error: {}\n", .{err});
};
}
}
},
else => {},
}
}
}
fn drawToolbar(canvas: *Canvas) void {
const tools_list = canvas.toolbar.tools;
if (tools_list.len == 0) return;
var bar = dvui.box(
@src(),
.{ .dir = .vertical },
.{
.gravity_x = 0.0,
.gravity_y = 0.0,
.margin = dvui.Rect{ .x = 8, .y = 8 },
.padding = dvui.Rect.all(6),
.corner_radius = dvui.Rect.all(8),
.background = true,
.color_fill = dvui.Color.black.opacity(0.2),
},
);
{
var to_select: ?usize = null;
for (tools_list, 0..) |*tool_desc, i| {
const is_selected = canvas.toolbar.selected_index == i;
const selected_fill = dvui.themeGet().focus;
const opts: dvui.Options = .{
.id_extra = i,
.color_fill = if (is_selected) selected_fill else null,
};
if (dvui.buttonIcon(@src(), tool_desc.name, tool_desc.icon_tvg, .{}, .{}, opts)) {
to_select = i;
}
}
if (to_select) |index| {
canvas.toolbar.select(index);
}
}
bar.deinit();
}