From 7923e378186f28a46786f6c8aac11b0e2874760b Mon Sep 17 00:00:00 2001 From: Roman Pytkov Date: Thu, 26 Feb 2026 20:03:04 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D1=80=D0=B0=D1=81=D0=BE=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Canvas.zig | 2 + src/WindowContext.zig | 1 + src/ui/canvas_view.zig | 247 ++++++++++++++++++++++++++++++++++++++++- src/ui/right_panel.zig | 2 +- 4 files changed, 250 insertions(+), 2 deletions(-) diff --git a/src/Canvas.zig b/src/Canvas.zig index 4658894..de92ee4 100644 --- a/src/Canvas.zig +++ b/src/Canvas.zig @@ -33,6 +33,8 @@ cursor_document_point: ?Point2_f = null, draw_document: bool = true, /// Rect тулбара (из предыдущего кадра) для исключения кликов по нему из handleCanvasMouse. toolbar_rect_scale: ?dvui.RectScale = null, +/// Rect панели свойств (из предыдущего кадра) для исключения кликов по нему из handleCanvasMouse. +properties_rect_scale: ?dvui.RectScale = null, pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine) Canvas { return .{ diff --git a/src/WindowContext.zig b/src/WindowContext.zig index 70f9e16..1dc8c05 100644 --- a/src/WindowContext.zig +++ b/src/WindowContext.zig @@ -24,6 +24,7 @@ pub const OpenDocument = struct { &self.document, (&self.cpu_render).renderEngine(), ); + self.selected_object = null; } pub fn deinit(self: *OpenDocument) void { diff --git a/src/ui/canvas_view.zig b/src/ui/canvas_view.zig index c68564c..83b4aef 100644 --- a/src/ui/canvas_view.zig +++ b/src/ui/canvas_view.zig @@ -2,10 +2,13 @@ const std = @import("std"); const dvui = @import("dvui"); const dvui_ext = @import("dvui_ext.zig"); const Canvas = @import("../Canvas.zig"); +const Document = @import("../models/Document.zig"); +const Property = @import("../models/Property.zig").Property; +const PropertyData = @import("../models/Property.zig").Data; const Rect_i = @import("../models/basic_models.zig").Rect_i; const Tool = @import("../toolbar/Tool.zig"); -pub fn canvasView(canvas: *Canvas, content_rect_scale: dvui.RectScale) void { +pub fn canvasView(canvas: *Canvas, selected_object: ?*Document.Object, 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 }); @@ -45,6 +48,27 @@ pub fn canvasView(canvas: *Canvas, content_rect_scale: dvui.RectScale) void { canvas.toolbar_rect_scale = toolbar_box.data().contentRectScale(); toolbar_box.deinit(); + // Панель свойств поверх scroll (правый верхний угол) + if (selected_object) |obj| { + var properties_box = dvui.box( + @src(), + .{ .dir = .horizontal }, + .{ + .expand = .none, + .background = false, + .gravity_x = 1.0, + .gravity_y = 0.0, + .margin = dvui.Rect{ .w = 8, .y = 8 }, + }, + ); + { + drawPropertiesPanel(canvas, obj); + } + // Сохраняем rect панели свойств для следующего кадра — в handleCanvasMouse исключаем из него клики + canvas.properties_rect_scale = properties_box.data().contentRectScale(); + properties_box.deinit(); + } + dvui.label(@src(), "Canvas", .{}, .{ .gravity_x = 0.5, .gravity_y = 0.0 }); } overlay.deinit(); @@ -181,6 +205,12 @@ fn handleCanvasMouse(canvas: *Canvas, scroll: *dvui.ScrollAreaWidget) void { 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; } + // Не обрабатывать клик, если он попал в область панели свойств (rect с предыдущего кадра). + if (canvas.properties_rect_scale) |prs| { + const pt = prs.pointFromPhysical(mouse.p); + const r = prs.r; + if (pt.x >= 0 and pt.x * prs.s < r.w and pt.y >= 0 and pt.y * prs.s < r.h) continue; + } const viewport_pt = scroll_data.contentRectScale().pointFromPhysical(mouse.p); const content_pt = dvui.Point{ @@ -242,3 +272,218 @@ fn drawToolbar(canvas: *Canvas) void { } bar.deinit(); } + +fn drawPropertiesPanel(canvas: *Canvas, selected_object: *Document.Object) void { + var panel = dvui.box( + @src(), + .{ .dir = .vertical }, + .{ + .gravity_x = 1.0, + .gravity_y = 0.0, + .margin = dvui.Rect{ .x = 8, .y = 8 }, + .padding = dvui.Rect.all(8), + .corner_radius = dvui.Rect.all(8), + .background = true, + .color_fill = dvui.Color.black.opacity(0.2), + .min_size_content = .{ .w = 220 }, + }, + ); + { + dvui.label(@src(), "Properties", .{}, .{}); + for (selected_object.properties.items, 0..) |*prop, i| { + drawPropertyEditor(canvas, selected_object, prop, i); + } + } + panel.deinit(); +} + +fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Property, row_index: usize) void { + const row_id: usize = row_index * 16; + var row = dvui.box( + @src(), + .{ .dir = .vertical }, + .{ + .id_extra = row_id, + .expand = .horizontal, + .padding = dvui.Rect{ .y = 2 }, + }, + ); + { + const tag = std.meta.activeTag(prop.data); + dvui.labelNoFmt(@src(), propertyLabel(tag), .{}, .{}); + + switch (prop.data) { + .position => |pos| { + var next = pos; + const doc = canvas.document; + const min_x = -doc.size.w; + const max_x = doc.size.w; + const min_y = -doc.size.h; + const max_y = doc.size.h; + var changed = false; + changed = dvui.sliderEntry(@src(), "x: {d:0.2}", .{ .value = &next.x, .min = min_x, .max = max_x, .interval = 0.1 }, .{ .expand = .horizontal }) or changed; + changed = dvui.sliderEntry(@src(), "y: {d:0.2}", .{ .value = &next.y, .min = min_y, .max = max_y, .interval = 0.1 }, .{ .expand = .horizontal }) or changed; + if (changed) { + obj.setProperty(canvas.document.allocator, .{ .data = .{ .position = next } }) catch {}; + canvas.requestRedraw(); + } + }, + .angle => |angle| { + var next = angle; + if (dvui.sliderEntry(@src(), "{d:0.2} rad", .{ .value = &next, .min = -std.math.pi * 2.0, .max = std.math.pi * 2.0, .interval = 0.01 }, .{ .expand = .horizontal })) { + obj.setProperty(canvas.document.allocator, .{ .data = .{ .angle = next } }) catch {}; + canvas.requestRedraw(); + } + }, + .scale => |scale| { + var next = scale; + var changed = false; + changed = dvui.sliderEntry(@src(), "x: {d:0.2}", .{ .value = &next.scale_x, .min = 0.0, .max = 10.0, .interval = 0.01 }, .{ .expand = .horizontal }) or changed; + changed = dvui.sliderEntry(@src(), "y: {d:0.2}", .{ .value = &next.scale_y, .min = 0.0, .max = 10.0, .interval = 0.01 }, .{ .expand = .horizontal }) or changed; + if (changed) { + obj.setProperty(canvas.document.allocator, .{ .data = .{ .scale = next } }) catch {}; + canvas.requestRedraw(); + } + }, + .visible => |v| { + var next = v; + if (dvui.checkbox(@src(), &next, "Visible", .{})) { + obj.setProperty(canvas.document.allocator, .{ .data = .{ .visible = next } }) catch {}; + canvas.requestRedraw(); + } + }, + .opacity => |opacity| { + var next = opacity; + if (dvui.sliderEntry(@src(), "{d:0.2}", .{ .value = &next, .min = 0.0, .max = 1.0, .interval = 0.01 }, .{ .expand = .horizontal })) { + obj.setProperty(canvas.document.allocator, .{ .data = .{ .opacity = next } }) catch {}; + canvas.requestRedraw(); + } + }, + .locked => |v| { + var next = v; + if (dvui.checkbox(@src(), &next, "Locked", .{})) { + obj.setProperty(canvas.document.allocator, .{ .data = .{ .locked = next } }) catch {}; + canvas.requestRedraw(); + } + }, + .size => |size| { + var next = size; + const doc = canvas.document; + var changed = false; + changed = dvui.sliderEntry(@src(), "w: {d:0.2}", .{ .value = &next.w, .min = 0.0, .max = doc.size.w, .interval = 1.0 }, .{ .expand = .horizontal }) or changed; + changed = dvui.sliderEntry(@src(), "h: {d:0.2}", .{ .value = &next.h, .min = 0.0, .max = doc.size.h, .interval = 1.0 }, .{ .expand = .horizontal }) or changed; + if (changed) { + obj.setProperty(canvas.document.allocator, .{ .data = .{ .size = next } }) catch {}; + canvas.requestRedraw(); + } + }, + .radii => |radii| { + var next = radii; + const doc = canvas.document; + var changed = false; + changed = dvui.sliderEntry(@src(), "x: {d:0.2}", .{ .value = &next.x, .min = 0.0, .max = doc.size.w, .interval = 1.0 }, .{ .expand = .horizontal }) or changed; + changed = dvui.sliderEntry(@src(), "y: {d:0.2}", .{ .value = &next.y, .min = 0.0, .max = doc.size.h, .interval = 1.0 }, .{ .expand = .horizontal }) or changed; + if (changed) { + obj.setProperty(canvas.document.allocator, .{ .data = .{ .radii = next } }) catch {}; + canvas.requestRedraw(); + } + }, + .end_point => |pt| { + var next = pt; + const doc = canvas.document; + const min_x = -doc.size.w; + const max_x = doc.size.w; + const min_y = -doc.size.h; + const max_y = doc.size.h; + var changed = false; + changed = dvui.sliderEntry(@src(), "x: {d:0.2}", .{ .value = &next.x, .min = min_x, .max = max_x, .interval = 0.1 }, .{ .expand = .horizontal }) or changed; + changed = dvui.sliderEntry(@src(), "y: {d:0.2}", .{ .value = &next.y, .min = min_y, .max = max_y, .interval = 0.1 }, .{ .expand = .horizontal }) or changed; + if (changed) { + obj.setProperty(canvas.document.allocator, .{ .data = .{ .end_point = next } }) catch {}; + canvas.requestRedraw(); + } + }, + .points => |points| { + dvui.label(@src(), "Points: {d}", .{points.items.len}, .{}); + }, + .fill_rgba => |rgba| { + drawColorEditor(canvas, obj, rgba, true); + }, + .stroke_rgba => |rgba| { + drawColorEditor(canvas, obj, rgba, false); + }, + .thickness => |t| { + var next = t; + if (dvui.sliderEntry(@src(), "{d:0.2}", .{ .value = &next, .min = 0.0, .max = 100.0, .interval = 0.1 }, .{ .expand = .horizontal })) { + obj.setProperty(canvas.document.allocator, .{ .data = .{ .thickness = next } }) catch {}; + canvas.requestRedraw(); + } + }, + } + } + row.deinit(); +} + +fn drawColorEditor(canvas: *Canvas, obj: *Document.Object, rgba: u32, is_fill: bool) void { + var comps = rgbaToComponents(rgba); + var changed = false; + changed = dvui.sliderEntry(@src(), "r: {d:0.0}", .{ .value = &comps.r, .min = 0, .max = 255, .interval = 1 }, .{ .expand = .horizontal }) or changed; + changed = dvui.sliderEntry(@src(), "g: {d:0.0}", .{ .value = &comps.g, .min = 0, .max = 255, .interval = 1 }, .{ .expand = .horizontal }) or changed; + changed = dvui.sliderEntry(@src(), "b: {d:0.0}", .{ .value = &comps.b, .min = 0, .max = 255, .interval = 1 }, .{ .expand = .horizontal }) or changed; + changed = dvui.sliderEntry(@src(), "a: {d:0.0}", .{ .value = &comps.a, .min = 0, .max = 255, .interval = 1 }, .{ .expand = .horizontal }) or changed; + if (changed) { + const next = componentsToRgba(comps); + if (is_fill) { + obj.setProperty(canvas.document.allocator, .{ .data = .{ .fill_rgba = next } }) catch {}; + } else { + obj.setProperty(canvas.document.allocator, .{ .data = .{ .stroke_rgba = next } }) catch {}; + } + canvas.requestRedraw(); + } +} + +fn propertyLabel(tag: std.meta.Tag(PropertyData)) []const u8 { + return switch (tag) { + .position => "Position", + .angle => "Angle", + .scale => "Scale", + .visible => "Visible", + .opacity => "Opacity", + .locked => "Locked", + .size => "Size", + .radii => "Radii", + .end_point => "End point", + .points => "Points", + .fill_rgba => "Fill color", + .stroke_rgba => "Stroke color", + .thickness => "Thickness", + }; +} + +const RgbaComponents = struct { + r: f32, + g: f32, + b: f32, + a: f32, +}; + +fn rgbaToComponents(rgba: u32) RgbaComponents { + return .{ + .r = @floatFromInt((rgba >> 24) & 0xFF), + .g = @floatFromInt((rgba >> 16) & 0xFF), + .b = @floatFromInt((rgba >> 8) & 0xFF), + .a = @floatFromInt((rgba >> 0) & 0xFF), + }; +} + +fn componentsToRgba(comps: RgbaComponents) u32 { + return (@as(u32, toByte(comps.r)) << 24) | + (@as(u32, toByte(comps.g)) << 16) | + (@as(u32, toByte(comps.b)) << 8) | + (@as(u32, toByte(comps.a)) << 0); +} + +fn toByte(value: f32) u8 { + const clamped = std.math.clamp(@round(value), 0.0, 255.0); + return @intFromFloat(clamped); +} diff --git a/src/ui/right_panel.zig b/src/ui/right_panel.zig index 4294b8f..1283113 100644 --- a/src/ui/right_panel.zig +++ b/src/ui/right_panel.zig @@ -26,7 +26,7 @@ pub fn rightPanel(ctx: *WindowContext) void { const active_doc = ctx.activeDocument(); if (active_doc) |doc| { const content_rect_scale = panel.data().contentRectScale(); - canvas_view.canvasView(&doc.canvas, content_rect_scale); + canvas_view.canvasView(&doc.canvas, doc.selected_object, content_rect_scale); } else { noDocView(ctx); }