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(); }