775 lines
32 KiB
Zig
775 lines
32 KiB
Zig
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 Point2_f = @import("../models/basic_models.zig").Point2_f;
|
||
const Tool = @import("../toolbar/Tool.zig");
|
||
const RenderStats = @import("../render/RenderStats.zig");
|
||
const icons = @import("../icons.zig");
|
||
|
||
pub fn canvasView(canvas: *Canvas, selected_object_id: ?u64, 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 });
|
||
{
|
||
const overlay_parent = dvui.parentGet();
|
||
const init_options: dvui.ScrollAreaWidget.InitOpts = .{
|
||
.scroll_info = &canvas.scroll,
|
||
.vertical_bar = .auto,
|
||
.horizontal_bar = .auto,
|
||
.process_events_after = false,
|
||
};
|
||
var scroll = dvui.scrollArea(
|
||
@src(),
|
||
init_options,
|
||
.{
|
||
.expand = .both,
|
||
.background = false,
|
||
},
|
||
);
|
||
{
|
||
drawCanvasContent(canvas, scroll);
|
||
handleCanvasZoom(canvas, scroll);
|
||
handleCanvasMouse(canvas, scroll, selected_object_id);
|
||
}
|
||
|
||
const scroll_parent = dvui.parentGet();
|
||
dvui.parentSet(overlay_parent);
|
||
|
||
const vbar = scroll.vbar;
|
||
const hbar = scroll.hbar;
|
||
if (vbar != null) {
|
||
|
||
// std.debug.print("{any}", .{vbar.?.data()});
|
||
}
|
||
if (hbar != null) {
|
||
// std.debug.print("{any}", .{hbar.?.data()});
|
||
}
|
||
|
||
// Тулбар поверх scroll
|
||
var toolbar_box = dvui.box(
|
||
@src(),
|
||
.{ .dir = .horizontal },
|
||
.{},
|
||
);
|
||
{
|
||
drawToolbar(canvas);
|
||
// Сохраняем rect тулбара для следующего кадра — в handleCanvasMouse исключаем из него клики
|
||
canvas.toolbar_rect_scale = toolbar_box.data().contentRectScale();
|
||
}
|
||
toolbar_box.deinit();
|
||
|
||
// Панель свойств поверх scroll (правый верхний угол)
|
||
if (selected_object_id) |obj_id| {
|
||
if (canvas.document.findObjectById(obj_id)) |obj| {
|
||
var properties_box = dvui.box(
|
||
@src(),
|
||
.{ .dir = .horizontal },
|
||
.{
|
||
.gravity_x = 1.0,
|
||
.gravity_y = 0.0,
|
||
},
|
||
);
|
||
{
|
||
drawPropertiesPanel(canvas, obj);
|
||
// Сохраняем rect панели свойств для следующего кадра — в handleCanvasMouse исключаем из него клики
|
||
canvas.properties_rect_scale = properties_box.data().contentRectScale();
|
||
}
|
||
properties_box.deinit();
|
||
}
|
||
}
|
||
|
||
drawCanvasLabelPanel();
|
||
if (canvas.show_render_stats)
|
||
drawStatsPanel(canvas.render_engine.getStats(), canvas.frame_index);
|
||
|
||
if (canvas.properties_rect_scale) |prs| {
|
||
for (dvui.events()) |*e| {
|
||
if (e.handled) continue;
|
||
if (e.evt != .mouse) continue;
|
||
const mouse = &e.evt.mouse;
|
||
if (mouse.action != .wheel_x and mouse.action != .wheel_y) continue;
|
||
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) {
|
||
e.handled = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!init_options.process_events_after) {
|
||
if (scroll.scroll) |*sc| {
|
||
dvui.clipSet(sc.prevClip);
|
||
sc.processEventsAfter();
|
||
}
|
||
}
|
||
|
||
dvui.parentSet(scroll_parent);
|
||
scroll.deinit();
|
||
}
|
||
overlay.deinit();
|
||
}
|
||
textured.deinit();
|
||
}
|
||
|
||
fn drawCanvasContent(canvas: *Canvas, scroll: anytype) void {
|
||
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
|
||
const img_size = canvas.getZoomedImageSize();
|
||
const viewport_rect = scroll.data().contentRect();
|
||
const scroll_current = dvui.Point{ .x = canvas.scroll.viewport.x, .y = canvas.scroll.viewport.y };
|
||
|
||
const viewport_px = dvui.Rect{
|
||
.x = viewport_rect.x * natural_scale,
|
||
.y = viewport_rect.y * natural_scale,
|
||
.w = viewport_rect.w * natural_scale,
|
||
.h = viewport_rect.h * natural_scale,
|
||
};
|
||
const scroll_px = dvui.Point{
|
||
.x = scroll_current.x * natural_scale,
|
||
.y = scroll_current.y * natural_scale,
|
||
};
|
||
|
||
const changed = canvas.updateVisibleImageRect(viewport_px, scroll_px);
|
||
if (changed)
|
||
canvas.requestRedraw();
|
||
canvas.processPendingRedraw() catch |err| {
|
||
std.debug.print("processPendingRedraw error: {}\n", .{err});
|
||
};
|
||
|
||
const content_w_px: u32 = img_size.x + img_size.w;
|
||
const content_h_px: u32 = img_size.y + img_size.h;
|
||
const content_w = @as(f32, @floatFromInt(content_w_px)) / natural_scale;
|
||
const content_h = @as(f32, @floatFromInt(content_h_px)) / natural_scale;
|
||
|
||
var canvas_layer = dvui.overlay(
|
||
@src(),
|
||
.{ .min_size_content = .{ .w = content_w, .h = content_h }, .background = false },
|
||
);
|
||
{
|
||
if (canvas.texture) |tex| {
|
||
const vis = canvas._visible_rect orelse Rect_i{ .x = 0, .y = 0, .w = 0, .h = 0 };
|
||
const left = @as(f32, @floatFromInt(img_size.x + vis.x)) / natural_scale;
|
||
const top = @as(f32, @floatFromInt(img_size.y + vis.y)) / natural_scale;
|
||
|
||
_ = dvui.image(
|
||
@src(),
|
||
.{ .source = .{ .texture = tex } },
|
||
.{
|
||
.background = false,
|
||
.expand = .none,
|
||
.gravity_x = 0.0,
|
||
.gravity_y = 0.0,
|
||
.margin = .{ .x = left, .y = top, .w = canvas.pos.x, .h = canvas.pos.y },
|
||
.min_size_content = .{
|
||
.w = @as(f32, @floatFromInt(vis.w)) / natural_scale,
|
||
.h = @as(f32, @floatFromInt(vis.h)) / natural_scale,
|
||
},
|
||
.max_size_content = .{
|
||
.w = @as(f32, @floatFromInt(vis.w)) / natural_scale,
|
||
.h = @as(f32, @floatFromInt(vis.h)) / natural_scale,
|
||
},
|
||
},
|
||
);
|
||
}
|
||
}
|
||
canvas_layer.deinit();
|
||
}
|
||
|
||
fn handleCanvasZoom(canvas: *Canvas, scroll: anytype) void {
|
||
const ctrl = dvui.currentWindow().modifiers.control();
|
||
if (!ctrl) return;
|
||
|
||
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
|
||
|
||
for (dvui.events()) |*e| {
|
||
switch (e.evt) {
|
||
.mouse => |*mouse| {
|
||
const action = mouse.action;
|
||
if (dvui.eventMatchSimple(e, scroll.data()) and (action == .wheel_x or action == .wheel_y)) {
|
||
switch (action) {
|
||
.wheel_y => |y| {
|
||
const viewport_pt = scroll.data().contentRectScale().pointFromPhysical(mouse.p);
|
||
const content_pt = dvui.Point{
|
||
.x = viewport_pt.x + canvas.scroll.viewport.x,
|
||
.y = viewport_pt.y + canvas.scroll.viewport.y,
|
||
};
|
||
const doc_pt = canvas.contentPointToDocument(content_pt, natural_scale);
|
||
|
||
// canvas.addZoom(y / 1000);
|
||
canvas.multZoom(1 + y / 2000);
|
||
canvas.requestRedraw();
|
||
|
||
const doc_pt_after = canvas.contentPointToDocument(content_pt, natural_scale);
|
||
|
||
const zoom = canvas.getZoom();
|
||
const dx = (doc_pt_after.x - doc_pt.x) * zoom / natural_scale;
|
||
const dy = (doc_pt_after.y - doc_pt.y) * zoom / natural_scale;
|
||
|
||
canvas.scroll.viewport.x -= dx;
|
||
canvas.scroll.viewport.y -= dy;
|
||
},
|
||
else => {},
|
||
}
|
||
e.handled = true;
|
||
}
|
||
},
|
||
else => {},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn handleCanvasMouse(canvas: *Canvas, scroll: *dvui.ScrollAreaWidget, selected_object_id: ?u64) void {
|
||
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
|
||
const scroll_data = scroll.data();
|
||
|
||
for (dvui.events()) |*e| {
|
||
switch (e.evt) {
|
||
.mouse => |*mouse| {
|
||
if (mouse.action != .press or mouse.button != .left) continue;
|
||
if (e.handled) continue;
|
||
if (!dvui.eventMatchSimple(e, scroll_data)) continue;
|
||
|
||
// Не обрабатывать клик, если он попал в область тулбара (rect с предыдущего кадра).
|
||
if (canvas.toolbar_rect_scale) |trs| {
|
||
const pt = trs.pointFromPhysical(mouse.p);
|
||
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{
|
||
.x = viewport_pt.x + canvas.scroll.viewport.x,
|
||
.y = viewport_pt.y + canvas.scroll.viewport.y,
|
||
};
|
||
const doc_pt = canvas.contentPointToDocument(content_pt, natural_scale);
|
||
canvas.cursor_document_point = if (canvas.isContentPointOnDocument(content_pt, natural_scale)) doc_pt else null;
|
||
if (canvas.cursor_document_point) |point| {
|
||
if (canvas.toolbar.currentDescriptor()) |desc| {
|
||
var ctx = Tool.ToolContext{
|
||
.canvas = canvas,
|
||
.document_point = point,
|
||
.selected_object_id = selected_object_id,
|
||
};
|
||
desc.implementation.onCanvasClick(&ctx) catch |err| {
|
||
std.debug.print("onCanvasClick error: {}\n", .{err});
|
||
};
|
||
}
|
||
}
|
||
},
|
||
else => {},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn drawToolbar(canvas: *Canvas) void {
|
||
const tools_list = canvas.toolbar.tools;
|
||
if (tools_list.len == 0) return;
|
||
|
||
var bar = dvui.box(
|
||
@src(),
|
||
.{ .dir = .vertical },
|
||
.{
|
||
.gravity_x = 0.0,
|
||
.gravity_y = 0.0,
|
||
.padding = dvui.Rect.all(6),
|
||
.corner_radius = dvui.Rect.all(8),
|
||
.background = true,
|
||
.color_fill = dvui.Color.black.opacity(0.2),
|
||
.margin = dvui.Rect{ .x = 16, .y = 16 },
|
||
},
|
||
);
|
||
{
|
||
var to_select: ?usize = null;
|
||
for (tools_list, 0..) |*tool_desc, i| {
|
||
const is_selected = canvas.toolbar.selected_index == i;
|
||
const selected_fill = dvui.themeGet().focus;
|
||
const opts: dvui.Options = .{
|
||
.id_extra = i,
|
||
.color_fill = if (is_selected) selected_fill else null,
|
||
};
|
||
if (dvui.buttonIcon(@src(), tool_desc.name, tool_desc.icon_tvg, .{}, .{}, opts)) {
|
||
to_select = i;
|
||
}
|
||
}
|
||
if (to_select) |index| {
|
||
canvas.toolbar.select(index);
|
||
}
|
||
}
|
||
bar.deinit();
|
||
}
|
||
|
||
fn drawPropertiesPanel(canvas: *Canvas, selected_object: *Document.Object) void {
|
||
var panel = dvui.box(
|
||
@src(),
|
||
.{ .dir = .vertical },
|
||
.{
|
||
.padding = dvui.Rect.all(8),
|
||
.corner_radius = dvui.Rect.all(8),
|
||
.background = true,
|
||
.color_fill = dvui.Color.black.opacity(0.2),
|
||
.min_size_content = .width(300),
|
||
.max_size_content = .width(300),
|
||
.margin = dvui.Rect{ .w = 32, .y = 16, .h = 100 },
|
||
},
|
||
);
|
||
{
|
||
dvui.label(@src(), "Properties", .{}, .{});
|
||
var scroll = dvui.scrollArea(@src(), .{
|
||
.horizontal = .none,
|
||
.vertical = .auto,
|
||
}, .{
|
||
.expand = .both,
|
||
});
|
||
{
|
||
for (selected_object.properties.items, 0..) |*prop, i| {
|
||
drawPropertyEditor(canvas, selected_object, prop, i);
|
||
}
|
||
}
|
||
scroll.deinit();
|
||
}
|
||
panel.deinit();
|
||
}
|
||
|
||
fn drawCanvasLabelPanel() void {
|
||
var panel = dvui.box(
|
||
@src(),
|
||
.{ .dir = .vertical },
|
||
.{
|
||
.gravity_x = 0.5,
|
||
.gravity_y = 0.0,
|
||
.padding = dvui.Rect.all(8),
|
||
.corner_radius = dvui.Rect.all(8),
|
||
.background = true,
|
||
.color_fill = dvui.Color.black.opacity(0.2),
|
||
.margin = dvui.Rect{ .x = 16, .y = 16 },
|
||
},
|
||
);
|
||
{
|
||
dvui.label(@src(), "Canvas", .{}, .{});
|
||
}
|
||
panel.deinit();
|
||
}
|
||
|
||
fn drawStatsPanel(stats: RenderStats, frame_index: u64) void {
|
||
var panel = dvui.box(
|
||
@src(),
|
||
.{ .dir = .vertical },
|
||
.{
|
||
.gravity_x = 0.0,
|
||
.gravity_y = 1.0,
|
||
.padding = dvui.Rect.all(8),
|
||
.corner_radius = dvui.Rect.all(8),
|
||
.background = true,
|
||
.color_fill = dvui.Color.black.opacity(0.2),
|
||
.margin = dvui.Rect{ .x = 16, .h = 16 },
|
||
},
|
||
);
|
||
{
|
||
dvui.label(@src(), "Frame time: {d:.2}ms", .{@as(f32, @floatFromInt(stats.render_time_ns)) / std.time.ns_per_ms}, .{});
|
||
dvui.label(@src(), "Frame index: {}", .{frame_index}, .{});
|
||
}
|
||
panel.deinit();
|
||
}
|
||
|
||
fn applyPropertyPatch(canvas: *Canvas, obj: *Document.Object, patch: Property) void {
|
||
obj.setProperty(canvas.allocator, patch) catch {};
|
||
canvas.requestRedraw();
|
||
}
|
||
|
||
fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Property, row_index: usize) void {
|
||
const row_id: usize = row_index * 16;
|
||
const is_even = row_index % 2 == 0;
|
||
var row = dvui.box(
|
||
@src(),
|
||
.{ .dir = .vertical },
|
||
.{
|
||
.id_extra = row_id,
|
||
.expand = .horizontal,
|
||
.padding = dvui.Rect{ .y = 2, .x = 4 },
|
||
.corner_radius = dvui.Rect.all(4),
|
||
.background = is_even,
|
||
.color_fill = if (is_even) dvui.Color.black.opacity(0.4) else .{},
|
||
},
|
||
);
|
||
{
|
||
const tag = std.meta.activeTag(prop.data);
|
||
|
||
dvui.labelNoFmt(@src(), propertyLabel(tag), .{}, .{});
|
||
|
||
switch (prop.data) {
|
||
.position => |pos| {
|
||
var next = pos;
|
||
var changed = false;
|
||
{
|
||
var subrow = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal });
|
||
dvui.labelNoFmt(@src(), "x:", .{}, .{});
|
||
const T = @TypeOf(next.x);
|
||
const res = dvui.textEntryNumber(@src(), T, .{ .value = &next.x }, .{ .expand = .horizontal });
|
||
subrow.deinit();
|
||
changed = res.changed or changed;
|
||
}
|
||
{
|
||
var subrow = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal });
|
||
dvui.labelNoFmt(@src(), "y:", .{}, .{});
|
||
const T = @TypeOf(next.y);
|
||
const res = dvui.textEntryNumber(@src(), T, .{ .value = &next.y }, .{ .expand = .horizontal });
|
||
subrow.deinit();
|
||
changed = res.changed or changed;
|
||
}
|
||
if (changed) {
|
||
applyPropertyPatch(canvas, obj, .{ .data = .{ .position = next } });
|
||
}
|
||
},
|
||
.angle => |angle| {
|
||
var next = angle;
|
||
{
|
||
var subrow = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal });
|
||
dvui.labelNoFmt(@src(), "deg:", .{}, .{});
|
||
var degrees: f32 = next * 180.0 / std.math.pi;
|
||
const res = dvui.textEntryNumber(@src(), f32, .{ .value = °rees }, .{ .expand = .horizontal });
|
||
subrow.deinit();
|
||
if (res.changed) {
|
||
next = degrees * std.math.pi / 180.0;
|
||
applyPropertyPatch(canvas, obj, .{ .data = .{ .angle = next } });
|
||
}
|
||
}
|
||
},
|
||
.scale => |scale| {
|
||
var next = scale;
|
||
var changed = false;
|
||
{
|
||
var subrow = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal });
|
||
dvui.labelNoFmt(@src(), "x:", .{}, .{});
|
||
const T = @TypeOf(next.scale_x);
|
||
const res = dvui.textEntryNumber(@src(), T, .{ .value = &next.scale_x, .min = @as(T, 0.0), .max = @as(T, 10.0) }, .{ .expand = .horizontal });
|
||
subrow.deinit();
|
||
changed = res.changed or changed;
|
||
}
|
||
{
|
||
var subrow = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal });
|
||
dvui.labelNoFmt(@src(), "y:", .{}, .{});
|
||
const T = @TypeOf(next.scale_y);
|
||
const res = dvui.textEntryNumber(@src(), T, .{ .value = &next.scale_y, .min = @as(T, 0.0), .max = @as(T, 10.0) }, .{ .expand = .horizontal });
|
||
subrow.deinit();
|
||
changed = res.changed or changed;
|
||
}
|
||
if (changed) {
|
||
applyPropertyPatch(canvas, obj, .{ .data = .{ .scale = next } });
|
||
}
|
||
},
|
||
.visible => |v| {
|
||
var next = v;
|
||
if (dvui.checkbox(@src(), &next, "Visible", .{})) {
|
||
applyPropertyPatch(canvas, obj, .{ .data = .{ .visible = next } });
|
||
}
|
||
},
|
||
.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 })) {
|
||
applyPropertyPatch(canvas, obj, .{ .data = .{ .opacity = next } });
|
||
}
|
||
},
|
||
.locked => |v| {
|
||
var next = v;
|
||
if (dvui.checkbox(@src(), &next, "Locked", .{})) {
|
||
applyPropertyPatch(canvas, obj, .{ .data = .{ .locked = next } });
|
||
}
|
||
},
|
||
.size => |size| {
|
||
var next = size;
|
||
var changed = false;
|
||
{
|
||
var subrow = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal });
|
||
dvui.labelNoFmt(@src(), "w:", .{}, .{});
|
||
const T = @TypeOf(next.w);
|
||
const res = dvui.textEntryNumber(@src(), T, .{ .value = &next.w, .min = @as(T, 0.0) }, .{ .expand = .horizontal });
|
||
subrow.deinit();
|
||
changed = res.changed or changed;
|
||
}
|
||
{
|
||
var subrow = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal });
|
||
dvui.labelNoFmt(@src(), "h:", .{}, .{});
|
||
const T = @TypeOf(next.h);
|
||
const res = dvui.textEntryNumber(@src(), T, .{ .value = &next.h, .min = @as(T, 0.0) }, .{ .expand = .horizontal });
|
||
subrow.deinit();
|
||
changed = res.changed or changed;
|
||
}
|
||
if (changed) {
|
||
applyPropertyPatch(canvas, obj, .{ .data = .{ .size = next } });
|
||
}
|
||
},
|
||
.radii => |radii| {
|
||
var next = radii;
|
||
var changed = false;
|
||
{
|
||
var subrow = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal });
|
||
dvui.labelNoFmt(@src(), "x:", .{}, .{});
|
||
const T = @TypeOf(next.x);
|
||
const res = dvui.textEntryNumber(@src(), T, .{ .value = &next.x, .min = @as(T, 0.0) }, .{ .expand = .horizontal });
|
||
subrow.deinit();
|
||
changed = res.changed or changed;
|
||
}
|
||
{
|
||
var subrow = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal });
|
||
dvui.labelNoFmt(@src(), "y:", .{}, .{});
|
||
const T = @TypeOf(next.y);
|
||
const res = dvui.textEntryNumber(@src(), T, .{ .value = &next.y, .min = @as(T, 0.0) }, .{ .expand = .horizontal });
|
||
subrow.deinit();
|
||
changed = res.changed or changed;
|
||
}
|
||
if (changed) {
|
||
applyPropertyPatch(canvas, obj, .{ .data = .{ .radii = next } });
|
||
}
|
||
},
|
||
.arc_percent => |pct| {
|
||
var next = pct;
|
||
if (dvui.sliderEntry(@src(), "{d:0.0}%", .{ .value = &next, .min = 0.0, .max = 100.0, .interval = 1.0 }, .{ .expand = .horizontal })) {
|
||
applyPropertyPatch(canvas, obj, .{ .data = .{ .arc_percent = next } });
|
||
}
|
||
},
|
||
.end_point => |pt| {
|
||
var next = pt;
|
||
var changed = false;
|
||
{
|
||
var subrow = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal });
|
||
dvui.labelNoFmt(@src(), "x:", .{}, .{});
|
||
const T = @TypeOf(next.x);
|
||
const res = dvui.textEntryNumber(@src(), T, .{ .value = &next.x }, .{ .expand = .horizontal });
|
||
subrow.deinit();
|
||
changed = res.changed or changed;
|
||
}
|
||
{
|
||
var subrow = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal });
|
||
dvui.labelNoFmt(@src(), "y:", .{}, .{});
|
||
const T = @TypeOf(next.y);
|
||
const res = dvui.textEntryNumber(@src(), T, .{ .value = &next.y }, .{ .expand = .horizontal });
|
||
subrow.deinit();
|
||
changed = res.changed or changed;
|
||
}
|
||
if (changed) {
|
||
applyPropertyPatch(canvas, obj, .{ .data = .{ .end_point = next } });
|
||
}
|
||
},
|
||
.points => |points| {
|
||
var list = std.ArrayList(Point2_f).empty;
|
||
list.appendSlice(canvas.allocator, points) catch {
|
||
dvui.label(@src(), "Points: {d}", .{points.len}, .{});
|
||
return;
|
||
};
|
||
defer list.deinit(canvas.allocator);
|
||
dvui.label(@src(), "Points: {d}", .{list.items.len}, .{});
|
||
|
||
var changed = false;
|
||
var to_delete: ?usize = null;
|
||
|
||
for (list.items, 0..) |*pt, i| {
|
||
// Одна строка: крестик удаления + paned с X/Y пополам
|
||
var subrow = dvui.box(
|
||
@src(),
|
||
.{ .dir = .horizontal },
|
||
.{
|
||
.expand = .horizontal,
|
||
.id_extra = i,
|
||
},
|
||
);
|
||
{
|
||
// Крестик удаления
|
||
if (dvui.buttonIcon(@src(), "Delete", icons.cross, .{}, .{}, .{
|
||
.id_extra = i,
|
||
.gravity_y = 0.5,
|
||
.margin = .{
|
||
.x = 8,
|
||
},
|
||
})) {
|
||
to_delete = i;
|
||
}
|
||
|
||
// Панель с X и Y, разделёнными пополам
|
||
var split_ratio: f32 = 0.5;
|
||
var paned = dvui.paned(
|
||
@src(),
|
||
.{
|
||
.direction = .horizontal,
|
||
.collapsed_size = 0.0,
|
||
.split_ratio = &split_ratio,
|
||
.handle_size = 0,
|
||
},
|
||
.{
|
||
.expand = .horizontal,
|
||
},
|
||
);
|
||
{
|
||
if (paned.showFirst()) {
|
||
var x_box = dvui.box(
|
||
@src(),
|
||
.{ .dir = .horizontal },
|
||
.{ .expand = .both },
|
||
);
|
||
{
|
||
dvui.labelNoFmt(@src(), "x:", .{}, .{
|
||
.gravity_y = 0.5,
|
||
});
|
||
const Tx = @TypeOf(pt.x);
|
||
const res_x = dvui.textEntryNumber(
|
||
@src(),
|
||
Tx,
|
||
.{ .value = &pt.x },
|
||
.{ .expand = .horizontal },
|
||
);
|
||
changed = res_x.changed or changed;
|
||
}
|
||
x_box.deinit();
|
||
}
|
||
|
||
if (paned.showSecond()) {
|
||
var y_box = dvui.box(
|
||
@src(),
|
||
.{ .dir = .horizontal },
|
||
.{ .expand = .both },
|
||
);
|
||
{
|
||
dvui.labelNoFmt(@src(), "y:", .{}, .{
|
||
.gravity_y = 0.5,
|
||
});
|
||
const Ty = @TypeOf(pt.y);
|
||
const res_y = dvui.textEntryNumber(
|
||
@src(),
|
||
Ty,
|
||
.{ .value = &pt.y },
|
||
.{ .expand = .horizontal },
|
||
);
|
||
changed = res_y.changed or changed;
|
||
}
|
||
y_box.deinit();
|
||
}
|
||
}
|
||
paned.deinit();
|
||
}
|
||
subrow.deinit();
|
||
}
|
||
|
||
// Удаление выбранной точки
|
||
if (to_delete) |idx| {
|
||
_ = list.orderedRemove(idx);
|
||
changed = true;
|
||
}
|
||
|
||
// Кнопка добавления новой точки (одна на весь список)
|
||
if (dvui.button(@src(), "Add point", .{}, .{})) {
|
||
const T = @TypeOf(list.items[0]);
|
||
const new_point: T = if (list.items.len > 0)
|
||
list.items[list.items.len - 1]
|
||
else
|
||
.{ .x = 0, .y = 0 };
|
||
list.append(canvas.allocator, new_point) catch {};
|
||
changed = true;
|
||
}
|
||
|
||
if (changed) {
|
||
const slice = canvas.allocator.dupe(Point2_f, list.items) catch return;
|
||
obj.setProperty(canvas.allocator, .{ .data = .{ .points = slice } }) catch {
|
||
canvas.allocator.free(slice);
|
||
return;
|
||
};
|
||
canvas.requestRedraw();
|
||
}
|
||
},
|
||
.fill_rgba => |rgba| {
|
||
drawColorEditor(canvas, obj, rgba, true);
|
||
},
|
||
.stroke_rgba => |rgba| {
|
||
drawColorEditor(canvas, obj, rgba, false);
|
||
},
|
||
.thickness => |t| {
|
||
var next = t;
|
||
{
|
||
var subrow = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal });
|
||
dvui.labelNoFmt(@src(), "thickness:", .{}, .{});
|
||
const T = @TypeOf(next);
|
||
const res = dvui.textEntryNumber(@src(), T, .{ .value = &next, .min = @as(T, 0.0), .max = @as(T, 100.0) }, .{ .expand = .horizontal });
|
||
subrow.deinit();
|
||
if (res.changed) {
|
||
applyPropertyPatch(canvas, obj, .{ .data = .{ .thickness = next } });
|
||
}
|
||
}
|
||
},
|
||
.closed => |v| {
|
||
var next = v;
|
||
if (dvui.checkbox(@src(), &next, "Closed", .{})) {
|
||
applyPropertyPatch(canvas, obj, .{ .data = .{ .closed = next } });
|
||
}
|
||
},
|
||
.filled => |v| {
|
||
var next = v;
|
||
if (dvui.checkbox(@src(), &next, "Filled", .{})) {
|
||
applyPropertyPatch(canvas, obj, .{ .data = .{ .filled = next } });
|
||
}
|
||
},
|
||
}
|
||
}
|
||
row.deinit();
|
||
}
|
||
|
||
fn drawColorEditor(canvas: *Canvas, obj: *Document.Object, rgba: u32, is_fill: bool) void {
|
||
var hsv = dvui.Color.HSV.fromColor(rgbaToColor(rgba));
|
||
if (dvui.colorPicker(
|
||
@src(),
|
||
.{ .hsv = &hsv, .dir = .horizontal, .sliders = .rgb, .alpha = true, .hex_text_entry = true },
|
||
.{ .expand = .horizontal },
|
||
)) {
|
||
const next = colorToRgba(hsv.toColor());
|
||
const patch: Property = if (is_fill)
|
||
.{ .data = .{ .fill_rgba = next } }
|
||
else
|
||
.{ .data = .{ .stroke_rgba = next } };
|
||
applyPropertyPatch(canvas, obj, patch);
|
||
}
|
||
}
|
||
|
||
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",
|
||
.arc_percent => "Arc %",
|
||
.end_point => "End point",
|
||
.points => "Points",
|
||
.fill_rgba => "Fill color",
|
||
.stroke_rgba => "Stroke color",
|
||
.thickness => "Thickness",
|
||
.closed => "Closed",
|
||
.filled => "Filled",
|
||
};
|
||
}
|
||
|
||
fn rgbaToColor(rgba: u32) dvui.Color {
|
||
return .{
|
||
.r = @intCast((rgba >> 24) & 0xFF),
|
||
.g = @intCast((rgba >> 16) & 0xFF),
|
||
.b = @intCast((rgba >> 8) & 0xFF),
|
||
.a = @intCast((rgba >> 0) & 0xFF),
|
||
};
|
||
}
|
||
|
||
fn colorToRgba(color: dvui.Color) u32 {
|
||
return (@as(u32, color.r) << 24) |
|
||
(@as(u32, color.g) << 16) |
|
||
(@as(u32, color.b) << 8) |
|
||
(@as(u32, color.a) << 0);
|
||
}
|