начало дерева объектов

This commit is contained in:
2026-02-26 15:16:43 +03:00
parent 05f5481a42
commit 93f7f3d814
4 changed files with 101 additions and 8 deletions

View File

@@ -12,6 +12,8 @@ pub const OpenDocument = struct {
document: Document, document: Document,
cpu_render: CpuRenderEngine, cpu_render: CpuRenderEngine,
canvas: Canvas, canvas: Canvas,
/// Выбранный объект в дереве (указатель, не индекс).
selected_object: ?*Document.Object = null,
pub fn init(allocator: std.mem.Allocator, self: *OpenDocument) void { pub fn init(allocator: std.mem.Allocator, self: *OpenDocument) void {
const default_size = basic_models.Size_f{ .w = 800, .h = 600 }; const default_size = basic_models.Size_f{ .w = 800, .h = 600 };

View File

@@ -4,3 +4,4 @@ pub const line = dvui.entypo.line_graph;
pub const ellipse = dvui.entypo.circle; pub const ellipse = dvui.entypo.circle;
pub const arc = dvui.entypo.loop; pub const arc = dvui.entypo.loop;
pub const broken = dvui.entypo.flow_line; pub const broken = dvui.entypo.flow_line;
pub const trash = dvui.entypo.trash;

View File

@@ -36,3 +36,28 @@ pub fn addShape(self: *Document, parent: ?*Object, shape_kind: Object.ShapeKind)
try self.addObject(obj); 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;
}

View File

@@ -1,6 +1,8 @@
const std = @import("std");
const dvui = @import("dvui"); const dvui = @import("dvui");
const WindowContext = @import("../WindowContext.zig"); const WindowContext = @import("../WindowContext.zig");
const Document = @import("../models/Document.zig"); const Document = @import("../models/Document.zig");
const icons = @import("../icons.zig");
const Object = Document.Object; const Object = Document.Object;
const panel_gap: f32 = 12; const panel_gap: f32 = 12;
@@ -8,6 +10,11 @@ const panel_padding: f32 = 5;
const panel_radius: f32 = 24; const panel_radius: f32 = 24;
const fill_color = dvui.Color.black.opacity(0.2); const fill_color = dvui.Color.black.opacity(0.2);
const ObjectTreeCallback = union(enum) {
select: *Object,
delete: *Object,
};
fn shapeLabel(shape: Object.ShapeKind) []const u8 { fn shapeLabel(shape: Object.ShapeKind) []const u8 {
return switch (shape) { return switch (shape) {
.line => "Line", .line => "Line",
@@ -17,34 +24,92 @@ fn shapeLabel(shape: Object.ShapeKind) []const u8 {
}; };
} }
fn objectTreeRow(obj: *const Object, depth: u32, row_id: *usize) void { fn objectTreeRow(open_doc: *WindowContext.OpenDocument, obj: *Object, depth: u32, object_callback: *?ObjectTreeCallback) void {
const id = row_id.*;
row_id.* += 1;
const indent_px = depth * 18; 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( var row = dvui.box(
@src(), @src(),
.{ .dir = .horizontal }, .{ .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(); row.deinit();
for (obj.children.items) |*child| { for (obj.children.items) |*child| {
objectTreeRow(child, depth + 1, row_id); objectTreeRow(open_doc, child, depth + 1, object_callback);
} }
} }
fn objectTree(ctx: *WindowContext) void { fn objectTree(ctx: *WindowContext) void {
const active_doc = ctx.activeDocument(); const active_doc = ctx.activeDocument();
var object_callback: ?ObjectTreeCallback = null;
if (active_doc) |open_doc| { if (active_doc) |open_doc| {
const doc = &open_doc.document; const doc = &open_doc.document;
if (doc.objects.items.len == 0) { if (doc.objects.items.len == 0) {
dvui.label(@src(), "No objects", .{}, .{}); dvui.label(@src(), "No objects", .{}, .{});
} else { } else {
var row_id: usize = 0;
for (doc.objects.items) |*obj| { 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 { } else {