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