diff --git a/assets/icons/.gitkeep b/assets/icons/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Canvas.zig b/src/Canvas.zig index c2455fb..051f1ac 100644 --- a/src/Canvas.zig +++ b/src/Canvas.zig @@ -9,11 +9,13 @@ const Size_i = basic_models.Size_i; const Point2_f = @import("models/basic_models.zig").Point2_f; const Color = dvui.Color; +const Toolbar = @import("Toolbar.zig"); const Canvas = @This(); allocator: std.mem.Allocator, document: *Document, render_engine: RenderEngine, +toolbar: Toolbar, texture: ?dvui.Texture = null, pos: dvui.Point = dvui.Point{ .x = 400, .y = 400 }, scroll: dvui.ScrollInfo = .{ @@ -30,15 +32,17 @@ cursor_document_point: ?Point2_f = null, /// true — рисовать документ (render), false — пример (gradient/squares). draw_document: bool = true, -pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine) Canvas { +pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine, toolbar: Toolbar) Canvas { return .{ .allocator = allocator, .document = document, .render_engine = engine, + .toolbar = toolbar, }; } pub fn deinit(self: *Canvas) void { + self.toolbar.deinit(); if (self.texture) |texture| { dvui.Texture.destroyLater(texture); self.texture = null; diff --git a/src/Toolbar.zig b/src/Toolbar.zig new file mode 100644 index 0000000..9b44df9 --- /dev/null +++ b/src/Toolbar.zig @@ -0,0 +1,36 @@ +//! Структура тулбара инструментов. Жизненный цикл совпадает с Canvas. + +const tool_interface = @import("tool_interface.zig"); + +const Toolbar = @This(); + +/// Описание одного инструмента для тулбара. +pub const ToolDescriptor = struct { + name: []const u8, + /// Иконка в формате TVG (байты). + icon_tvg: []const u8, + /// Реализация интерфейса инструмента (своя для каждого инструмента в tools/). + implementation: *const tool_interface.Tool, +}; + +/// Вертикальный тулбар инструментов. +tools: []const ToolDescriptor, +selected_index: usize, + +pub fn init(tools_list: []const ToolDescriptor) Toolbar { + return .{ + .tools = tools_list, + .selected_index = 0, + }; +} + +pub fn deinit(_: *Toolbar) void {} + +pub fn currentDescriptor(self: *const Toolbar) ?*const ToolDescriptor { + if (self.tools.len == 0) return null; + return &self.tools[self.selected_index]; +} + +pub fn select(self: *Toolbar, index: usize) void { + if (index < self.tools.len) self.selected_index = index; +} diff --git a/src/WindowContext.zig b/src/WindowContext.zig index 6fb6733..9f0d919 100644 --- a/src/WindowContext.zig +++ b/src/WindowContext.zig @@ -5,6 +5,8 @@ const RenderEngine = @import("render/RenderEngine.zig").RenderEngine; const Document = @import("models/Document.zig"); const random_document = @import("models/random_document.zig"); const basic_models = @import("models/basic_models.zig"); +const tools = @import("tools.zig"); +const Toolbar = @import("Toolbar.zig"); const WindowContext = @This(); @@ -17,7 +19,12 @@ pub const OpenDocument = struct { const default_size = basic_models.Size_f{ .w = 800, .h = 600 }; self.document = Document.init(allocator, default_size); self.cpu_render = CpuRenderEngine.init(allocator, .Squares); - self.canvas = Canvas.init(allocator, &self.document, (&self.cpu_render).renderEngine()); + self.canvas = Canvas.init( + allocator, + &self.document, + (&self.cpu_render).renderEngine(), + Toolbar.init(&tools.default_tools), + ); } pub fn deinit(self: *OpenDocument) void { diff --git a/src/icons.zig b/src/icons.zig new file mode 100644 index 0000000..aff6e5a --- /dev/null +++ b/src/icons.zig @@ -0,0 +1,3 @@ +const dvui = @import("dvui"); + +pub const line = dvui.entypo.line_graph; diff --git a/src/tool_interface.zig b/src/tool_interface.zig new file mode 100644 index 0000000..aeb1ffb --- /dev/null +++ b/src/tool_interface.zig @@ -0,0 +1,14 @@ +//! Общий интерфейс инструмента. Реализации живут в каталоге tools/. + +const Point2_f = @import("models/basic_models.zig").Point2_f; + +/// Контекст вызова: холст и точка в координатах документа. +pub const ToolContext = struct { + canvas: *anyopaque, + document_point: Point2_f, +}; + +/// Интерфейс инструмента: один метод — клик по холсту в позиции курсора. +pub const Tool = struct { + onClick: *const fn (*const ToolContext) void, +}; diff --git a/src/tools.zig b/src/tools.zig new file mode 100644 index 0000000..fdc8152 --- /dev/null +++ b/src/tools.zig @@ -0,0 +1,13 @@ +//! Список инструментов по умолчанию для тулбара. Реализации — в каталоге tools/. + +const Toolbar = @import("Toolbar.zig"); +const line = @import("tools/line.zig"); +const icons = @import("icons.zig"); + +pub const default_tools = [_]Toolbar.ToolDescriptor{ + .{ + .name = "Line", + .icon_tvg = icons.line, + .implementation = &line.tool, + }, +}; diff --git a/src/tools/line.zig b/src/tools/line.zig new file mode 100644 index 0000000..e02958f --- /dev/null +++ b/src/tools/line.zig @@ -0,0 +1,21 @@ +//! Инструмент «Линия»: создаёт линию в позиции клика. + +const Canvas = @import("../Canvas.zig"); +const tool_interface = @import("../tool_interface.zig"); +const shape = @import("../models/shape/shape.zig"); + +fn onClick(ctx: *const tool_interface.ToolContext) void { + const canvas: *Canvas = @alignCast(@ptrCast(ctx.canvas)); + var obj = shape.createObject(canvas.document.allocator, .line) catch return; + obj.setProperty(canvas.document.allocator, .{ .data = .{ .position = ctx.document_point } }) catch { + obj.deinit(canvas.document.allocator); + return; + }; + canvas.document.addObject(obj) catch { + obj.deinit(canvas.document.allocator); + return; + }; + canvas.requestRedraw(); +} + +pub const tool = tool_interface.Tool{ .onClick = onClick }; diff --git a/src/ui/canvas_view.zig b/src/ui/canvas_view.zig index e067b1e..8ef3f39 100644 --- a/src/ui/canvas_view.zig +++ b/src/ui/canvas_view.zig @@ -3,6 +3,7 @@ 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_interface = @import("../tool_interface.zig"); pub fn canvasView(canvas: *Canvas, content_rect_scale: dvui.RectScale) void { var textured = dvui_ext.texturedBox(content_rect_scale, dvui.Rect.all(20)); @@ -25,6 +26,7 @@ pub fn canvasView(canvas: *Canvas, content_rect_scale: dvui.RectScale) void { } scroll.deinit(); + drawToolbar(canvas); dvui.label(@src(), "Canvas", .{}, .{ .gravity_x = 0.5, .gravity_y = 0.0 }); } overlay.deinit(); @@ -159,10 +161,49 @@ fn handleCanvasMouse(canvas: *Canvas, scroll: anytype) void { }; 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| - std.debug.print("cursor_document_point: {}\n", .{point}); + if (canvas.cursor_document_point) |point| { + if (canvas.toolbar.currentDescriptor()) |desc| { + var ctx = tool_interface.ToolContext{ + .canvas = canvas, + .document_point = point, + }; + desc.implementation.onClick(&ctx); + } + } }, 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), + }, + ); + { + for (tools_list, 0..) |*tool_desc, i| { + const is_selected = (canvas.toolbar.selected_index == i); + const opts: dvui.Options = .{ + .id_extra = i, + .color_fill = if (is_selected) dvui.Color.transparent else undefined, + }; + if (dvui.buttonIcon(@src(), tool_desc.name, tool_desc.icon_tvg, .{}, .{}, opts)) { + canvas.toolbar.select(i); + } + } + } + bar.deinit(); +}