Files
Zivro/src/ui/canvas_view.zig
2026-03-04 02:00:40 +03:00

775 lines
32 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = &degrees }, .{ .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);
}