Красота

This commit is contained in:
2026-02-26 20:03:04 +03:00
parent 9e2bb23c8a
commit 7923e37818
4 changed files with 250 additions and 2 deletions

View File

@@ -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);
}

View File

@@ -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);
}