From 93f7f3d8143b492d70554cdfb1e14c7e2d607b35 Mon Sep 17 00:00:00 2001 From: Roman Pytkov Date: Thu, 26 Feb 2026 15:16:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B0=D0=BB=D0=BE=20=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B2=D0=B0=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/WindowContext.zig | 2 + src/icons.zig | 1 + src/models/Document.zig | 25 +++++++++++++ src/ui/left_panel.zig | 81 +++++++++++++++++++++++++++++++++++++---- 4 files changed, 101 insertions(+), 8 deletions(-) diff --git a/src/WindowContext.zig b/src/WindowContext.zig index 77776de..70f9e16 100644 --- a/src/WindowContext.zig +++ b/src/WindowContext.zig @@ -12,6 +12,8 @@ pub const OpenDocument = struct { document: Document, cpu_render: CpuRenderEngine, canvas: Canvas, + /// Выбранный объект в дереве (указатель, не индекс). + selected_object: ?*Document.Object = null, pub fn init(allocator: std.mem.Allocator, self: *OpenDocument) void { const default_size = basic_models.Size_f{ .w = 800, .h = 600 }; diff --git a/src/icons.zig b/src/icons.zig index 97cd3a8..1f0f5e9 100644 --- a/src/icons.zig +++ b/src/icons.zig @@ -4,3 +4,4 @@ pub const line = dvui.entypo.line_graph; pub const ellipse = dvui.entypo.circle; pub const arc = dvui.entypo.loop; pub const broken = dvui.entypo.flow_line; +pub const trash = dvui.entypo.trash; diff --git a/src/models/Document.zig b/src/models/Document.zig index 733fe2f..f9905aa 100644 --- a/src/models/Document.zig +++ b/src/models/Document.zig @@ -36,3 +36,28 @@ pub fn addShape(self: *Document, parent: ?*Object, shape_kind: Object.ShapeKind) try self.addObject(obj); } } + +/// Удаляет объект из документа (из корня или из детей родителя). Возвращает true, если объект был найден и удалён. +pub fn removeObject(self: *Document, obj: *Object) bool { + for (self.objects.items, 0..) |*item, i| { + if (item == obj) { + var removed = self.objects.orderedRemove(i); + removed.deinit(self.allocator); + return true; + } + if (removeFromChildren(self.allocator, &item.children, obj)) return true; + } + return false; +} + +fn removeFromChildren(allocator: std.mem.Allocator, children: *std.ArrayList(Object), obj: *Object) bool { + for (children.items, 0..) |*item, i| { + if (item == obj) { + var removed = children.orderedRemove(i); + removed.deinit(allocator); + return true; + } + if (removeFromChildren(allocator, &item.children, obj)) return true; + } + return false; +} diff --git a/src/ui/left_panel.zig b/src/ui/left_panel.zig index 7735288..ef9cf42 100644 --- a/src/ui/left_panel.zig +++ b/src/ui/left_panel.zig @@ -1,6 +1,8 @@ +const std = @import("std"); const dvui = @import("dvui"); const WindowContext = @import("../WindowContext.zig"); const Document = @import("../models/Document.zig"); +const icons = @import("../icons.zig"); const Object = Document.Object; const panel_gap: f32 = 12; @@ -8,6 +10,11 @@ const panel_padding: f32 = 5; const panel_radius: f32 = 24; const fill_color = dvui.Color.black.opacity(0.2); +const ObjectTreeCallback = union(enum) { + select: *Object, + delete: *Object, +}; + fn shapeLabel(shape: Object.ShapeKind) []const u8 { return switch (shape) { .line => "Line", @@ -17,34 +24,92 @@ fn shapeLabel(shape: Object.ShapeKind) []const u8 { }; } -fn objectTreeRow(obj: *const Object, depth: u32, row_id: *usize) void { - const id = row_id.*; - row_id.* += 1; +fn objectTreeRow(open_doc: *WindowContext.OpenDocument, obj: *Object, depth: u32, object_callback: *?ObjectTreeCallback) void { const indent_px = depth * 18; + const is_selected: bool = open_doc.selected_object == obj; + const row_id = @intFromPtr(obj); + + const focus_color = dvui.themeGet().focus; + + // Визуально строка — это box с подсветкой по hover/selected, а не кнопка. var row = dvui.box( @src(), .{ .dir = .horizontal }, - .{ .padding = dvui.Rect{ .x = @floatFromInt(indent_px) }, .id_extra = id }, + .{ + .id_extra = row_id, + .expand = .horizontal, + // Постоянной рамки нет; лёгкая заливка по hover и более яркая по selected. + // .color_fill = if (is_selected) focus_color.opacity(0.35) else null, + .color_fill_hover = focus_color.opacity(0.18), + }, ); { - dvui.labelNoFmt(@src(), shapeLabel(obj.shape), .{}, .{ .id_extra = id }); + var hovered: bool = false; + const row_data = row.data(); + if (dvui.clicked(row_data, .{ + .hovered = &hovered, + })) { + object_callback.* = .{ .select = obj }; + } + + const background = is_selected or hovered; + var content = dvui.box(@src(), .{ .dir = .horizontal }, .{ + .expand = .horizontal, + .margin = dvui.Rect{ .x = @floatFromInt(indent_px) }, + .background = background, + .color_fill = if (is_selected) focus_color.opacity(0.35) else if (hovered) focus_color.opacity(0.18) else null, + }); + { + dvui.labelNoFmt( + @src(), + shapeLabel(obj.shape), + .{}, + .{ .id_extra = row_id, .expand = .horizontal }, + ); + + if (is_selected or hovered) { + const delete_opts: dvui.Options = .{ + .id_extra = row_id +% 1, + .margin = dvui.Rect{ .x = 4 }, + .padding = dvui.Rect.all(2), + .gravity_y = 0.5, + .gravity_x = 1.0, + }; + if (dvui.buttonIcon(@src(), "Delete object", icons.trash, .{}, .{}, delete_opts)) { + object_callback.* = .{ .delete = obj }; + } + } + } + content.deinit(); } row.deinit(); for (obj.children.items) |*child| { - objectTreeRow(child, depth + 1, row_id); + objectTreeRow(open_doc, child, depth + 1, object_callback); } } fn objectTree(ctx: *WindowContext) void { const active_doc = ctx.activeDocument(); + var object_callback: ?ObjectTreeCallback = null; if (active_doc) |open_doc| { const doc = &open_doc.document; if (doc.objects.items.len == 0) { dvui.label(@src(), "No objects", .{}, .{}); } else { - var row_id: usize = 0; for (doc.objects.items) |*obj| { - objectTreeRow(obj, 0, &row_id); + objectTreeRow(open_doc, obj, 0, &object_callback); + } + } + if (object_callback) |callback| { + switch (callback) { + .select => |obj| { + open_doc.selected_object = obj; + }, + .delete => |obj| { + _ = doc.removeObject(obj); + open_doc.selected_object = null; + open_doc.canvas.requestRedraw(); + }, } } } else {