244 lines
10 KiB
Zig
244 lines
10 KiB
Zig
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();
|
||
}
|