From 1a94cc8bfdb2216be0a7fe6e3f28a47793ddc04d Mon Sep 17 00:00:00 2001 From: Roman Pytkov Date: Tue, 24 Feb 2026 00:39:40 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BB=D1=83=D1=87=D0=B0=D0=B9=D0=BD?= =?UTF-8?q?=D0=BE=D0=B5=20=D0=B4=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/WindowContext.zig | 1 + src/models/Document.zig | 40 ++++++++++++ src/models/Object.zig | 2 +- src/ui/frame.zig | 10 +-- src/ui/left_panel.zig | 132 +++++++++++++++++++++++++++++++++++----- src/ui/right_panel.zig | 66 +++++++++++++------- 6 files changed, 203 insertions(+), 48 deletions(-) diff --git a/src/WindowContext.zig b/src/WindowContext.zig index b715a6e..3b77b47 100644 --- a/src/WindowContext.zig +++ b/src/WindowContext.zig @@ -60,6 +60,7 @@ pub fn addNewDocument(self: *WindowContext) !void { const ptr = try self.allocator.create(OpenDocument); errdefer self.allocator.destroy(ptr); OpenDocument.init(self.allocator, ptr); + try ptr.document.addRandomShapes(std.crypto.random); try self.documents.append(self.allocator, ptr); self.active_document_index = self.documents.items.len - 1; } diff --git a/src/models/Document.zig b/src/models/Document.zig index 72a27cf..5f647ea 100644 --- a/src/models/Document.zig +++ b/src/models/Document.zig @@ -40,3 +40,43 @@ pub fn addShape(self: *Document, parent: ?*Object, shape: Object.ShapeKind) !voi try self.addObject(obj); } } + +fn randomShapeKind(rng: std.Random) Object.ShapeKind { + const shapes_implemented = [_]Object.ShapeKind{ .line, .ellipse, .broken }; + return shapes_implemented[rng.intRangeLessThan(usize, 0, shapes_implemented.len)]; +} + +/// Создаёт случайное количество фигур в документе (в т.ч. вложенных). +/// Используются только реализованные типы: line, ellipse, broken. +/// Ограничение max_total предотвращает экспоненциальный рост и переполнение. +pub fn addRandomShapes(self: *Document, rng: std.Random) !void { + const max_total: usize = 80; + var total_count: usize = 0; + + const n_root = rng.intRangeLessThan(usize, 1, 5); + for (0..n_root) |_| { + if (total_count >= max_total) break; + try self.addShape(null, randomShapeKind(rng)); + total_count += 1; + } + + var stack = std.ArrayList(*Object).empty; + defer stack.deinit(self.allocator); + for (self.objects.items) |*obj| { + try stack.append(self.allocator, obj); + } + while (stack.pop()) |obj| { + if (total_count >= max_total) continue; + const n_children = rng.intRangeLessThan(usize, 0, 2); + const base_len = obj.children.items.len; + for (0..n_children) |_| { + if (total_count >= max_total) break; + try self.addShape(obj, randomShapeKind(rng)); + total_count += 1; + } + // Пушим в стек только после всех append, чтобы не держать указатели при реаллокации obj.children + for (obj.children.items[base_len..]) |*child| { + try stack.append(self.allocator, child); + } + } +} diff --git a/src/models/Object.zig b/src/models/Object.zig index 5566d6c..8831e7b 100644 --- a/src/models/Object.zig +++ b/src/models/Object.zig @@ -97,7 +97,7 @@ pub fn createLine(allocator: std.mem.Allocator) !Object { pub fn createBrokenLine(allocator: std.mem.Allocator) !Object { var obj = try createWithCommonProperties(allocator, .broken); errdefer obj.deinit(allocator); - var points = std.ArrayList(Point2).init(allocator); + var points = std.ArrayList(Point2).empty; try points.appendSlice(allocator, &.{ .{ .x = 0, .y = 0 }, .{ .x = 80, .y = 0 }, diff --git a/src/ui/frame.zig b/src/ui/frame.zig index fb35ec7..b571dd3 100644 --- a/src/ui/frame.zig +++ b/src/ui/frame.zig @@ -22,15 +22,7 @@ pub fn guiFrame(ctx: *WindowContext) bool { { left_panel.leftPanel(ctx); - var back = dvui.box( - @src(), - .{ .dir = .horizontal }, - .{ .expand = .both, .padding = dvui.Rect.all(12), .background = true }, - ); - { - right_panel.rightPanel(ctx); - } - back.deinit(); + right_panel.rightPanel(ctx); } content_row.deinit(); } diff --git a/src/ui/left_panel.zig b/src/ui/left_panel.zig index 7de4bfc..2f2770a 100644 --- a/src/ui/left_panel.zig +++ b/src/ui/left_panel.zig @@ -1,30 +1,130 @@ const dvui = @import("dvui"); const WindowContext = @import("../WindowContext.zig"); +const Document = @import("../models/Document.zig"); +const Object = Document.Object; + +const panel_gap: f32 = 12; +const panel_padding: f32 = 5; +const panel_radius: f32 = 24; +const fill_color = dvui.Color.black.opacity(0.2); + +fn shapeLabel(shape: Object.ShapeKind) []const u8 { + return switch (shape) { + .line => "Line", + .ellipse => "Ellipse", + .arc => "Arc", + .broken => "Broken line", + }; +} + +fn objectTreeRow(obj: *const Object, depth: u32, row_id: *usize) void { + const id = row_id.*; + row_id.* += 1; + const indent_px = depth * 18; + var row = dvui.box( + @src(), + .{ .dir = .horizontal }, + .{ .padding = dvui.Rect{ .x = @floatFromInt(indent_px) }, .id_extra = id }, + ); + { + dvui.labelNoFmt(@src(), shapeLabel(obj.shape), .{}, .{ .id_extra = id }); + } + row.deinit(); + for (obj.children.items) |*child| { + objectTreeRow(child, depth + 1, row_id); + } +} + +fn objectTree(ctx: *WindowContext) void { + const active_doc = ctx.activeDocument(); + 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); + } + } + } else { + dvui.label(@src(), "No document", .{}, .{}); + } +} pub fn leftPanel(ctx: *WindowContext) void { + var padding = dvui.Rect.all(panel_gap); + padding.w = 0; var panel = dvui.box( @src(), .{ .dir = .vertical }, - .{ .expand = .vertical, .min_size_content = .{ .w = 200 }, .background = true }, + .{ + .expand = .vertical, + .min_size_content = .{ .w = 220 }, + .background = true, + .padding = padding, + }, ); { - dvui.label(@src(), "Tools", .{}, .{}); - - const active_doc = ctx.activeDocument(); - if (active_doc) |doc| { - const canvas = &doc.canvas; - if (dvui.checkbox(@src(), &canvas.native_scaling, "Scaling", .{})) {} - if (dvui.button(@src(), if (doc.cpu_render.type == .Gradient) "Gradient" else "Squares", .{}, .{})) { - if (doc.cpu_render.type == .Gradient) { - doc.cpu_render.type = .Squares; - } else { - doc.cpu_render.type = .Gradient; - } - canvas.redrawExample() catch {}; + // Верхняя часть: дерево объектов + var tree_section = dvui.box( + @src(), + .{ .dir = .vertical }, + .{ + .expand = .both, + .padding = dvui.Rect.all(panel_padding), + .corner_radius = dvui.Rect.all(panel_radius), + .color_fill = fill_color, + .background = true, + }, + ); + { + dvui.label(@src(), "Objects", .{}, .{}); + var scroll = dvui.scrollArea( + @src(), + .{ .vertical = .auto }, + .{ .expand = .vertical, .background = false }, + ); + { + objectTree(ctx); } - } else { - dvui.label(@src(), "No document", .{}, .{}); + scroll.deinit(); } + tree_section.deinit(); + + // Нижняя часть: настройки + var settings_section = dvui.box( + @src(), + .{ .dir = .vertical }, + .{ + .expand = .horizontal, + .margin = .{ .y = 5 }, + .padding = dvui.Rect.all(panel_padding), + .corner_radius = dvui.Rect.all(panel_radius), + .color_fill = fill_color, + .background = true, + }, + ); + { + dvui.label(@src(), "Settings", .{}, .{}); + + const active_doc = ctx.activeDocument(); + if (active_doc) |doc| { + const canvas = &doc.canvas; + if (dvui.checkbox(@src(), &canvas.native_scaling, "Scaling", .{})) {} + if (dvui.button(@src(), if (doc.cpu_render.type == .Gradient) "Gradient" else "Squares", .{}, .{})) { + if (doc.cpu_render.type == .Gradient) { + doc.cpu_render.type = .Squares; + } else { + doc.cpu_render.type = .Gradient; + } + canvas.redrawExample() catch {}; + } + } else { + dvui.label(@src(), "No document", .{}, .{}); + } + } + settings_section.deinit(); } panel.deinit(); } diff --git a/src/ui/right_panel.zig b/src/ui/right_panel.zig index 887bec6..4294b8f 100644 --- a/src/ui/right_panel.zig +++ b/src/ui/right_panel.zig @@ -5,42 +5,64 @@ const canvas_view = @import("canvas_view.zig"); pub fn rightPanel(ctx: *WindowContext) void { const fill_color = dvui.Color.black.opacity(0.25); - var panel = dvui.box( + var back = dvui.box( @src(), - .{ .dir = .vertical }, - .{ - .expand = .both, - .background = true, - .padding = dvui.Rect.all(5), - .corner_radius = dvui.Rect.all(24), - .color_fill = fill_color, - }, + .{ .dir = .horizontal }, + .{ .expand = .both, .padding = dvui.Rect.all(12), .background = true }, ); { - const active_doc = ctx.activeDocument(); - if (active_doc) |doc| { - const content_rect_scale = panel.data().contentRectScale(); - canvas_view.canvasView(&doc.canvas, content_rect_scale); - } else { - noDocView(ctx); + var panel = dvui.box( + @src(), + .{ .dir = .vertical }, + .{ + .expand = .both, + .background = true, + .padding = dvui.Rect.all(5), + .corner_radius = dvui.Rect.all(24), + .color_fill = fill_color, + }, + ); + { + const active_doc = ctx.activeDocument(); + if (active_doc) |doc| { + const content_rect_scale = panel.data().contentRectScale(); + canvas_view.canvasView(&doc.canvas, content_rect_scale); + } else { + noDocView(ctx); + } } + panel.deinit(); } - panel.deinit(); + back.deinit(); } fn noDocView(ctx: *WindowContext) void { var center = dvui.box( @src(), .{ .dir = .vertical }, - .{ .expand = .both, .padding = dvui.Rect.all(20) }, + .{ + .expand = .both, + .padding = dvui.Rect.all(20), + }, ); { - dvui.label(@src(), "No document open", .{}, .{}); - if (dvui.button(@src(), "New document", .{}, .{})) { - ctx.addNewDocument() catch |err| { - std.debug.print("addNewDocument error: {}\n", .{err}); - }; + var box = dvui.box( + @src(), + .{ .dir = .vertical }, + .{ + .gravity_x = 0.5, + .gravity_y = 0.5, + }, + ); + { + dvui.label(@src(), "No document open", .{}, .{}); + if (dvui.button(@src(), "New document", .{}, .{})) { + ctx.addNewDocument() catch |err| { + std.debug.print("addNewDocument error: {}\n", .{err}); + }; + } } + box.deinit(); } center.deinit(); }