Compare commits

..

9 Commits

Author SHA1 Message Date
7145b66e0e Merge remote-tracking branch 'codeberg/review' 2026-03-18 23:39:28 +03:00
andrew.kraevskii
d6c8a062cd Fix memory leaks found by DebugAllocator. 2026-03-04 02:00:40 +03:00
andrew.kraevskii
045624beac Use leak detection of DebugAllocator.
When allocating memory it remebers where memory was allocated but it only
for leaks in .deinit(). So not calling it is a sure way to miss a bunch of leaks.

deinit() returns if it found leaks but we don't really care. Its usefull for tests but not here.
2026-03-04 02:00:40 +03:00
andrew.kraevskii
b0259c5788 Stuff. 2026-03-04 02:00:40 +03:00
andrew.kraevskii
0a47ea1e43 If function doesn't do anything related to object just don't pass object to it 2026-03-04 02:00:40 +03:00
andrew.kraevskii
cc10d806fe Return type of field of property instead of property. 2026-03-04 02:00:40 +03:00
andrew.kraevskii
e3a4506194 Use std.mem.Allocator.dupe instead of @memcpy. 2026-03-04 02:00:40 +03:00
andrew.kraevskii
3348b2e91c Update to latest dvui. 2026-03-04 02:00:40 +03:00
andrew.kraevskii
9ca360c6b3 Im trying not to die of old age waiting on llvm :( 2026-03-04 02:00:21 +03:00
17 changed files with 67 additions and 72 deletions

View File

@@ -8,8 +8,8 @@ pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "Zivro",
.use_llvm = true,
.use_lld = true,
// .use_llvm = true,
// .use_lld = true,
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,

View File

@@ -33,8 +33,8 @@
// internet connectivity.
.dependencies = .{
.dvui = .{
.url = "https://github.com/david-vanderson/dvui/archive/main.tar.gz",
.hash = "dvui-0.4.0-dev-AQFJmev72QC6e0ojhnW8a_wRhZDgzWWLgeyoNuIPZc2m",
.url = "git+https://github.com/david-vanderson/dvui#edb2d5a4cd2981fca74ee5f096277b91333c1316",
.hash = "dvui-0.4.0-dev-AQFJmeGB3QAwun9qF76CDE5IopA4nUVRgD-IwwTsOo4H",
},
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.

0
review.txt Normal file
View File

View File

@@ -16,15 +16,10 @@ pub const OpenDocument = struct {
selected_object_id: ?u64 = null,
pub fn init(allocator: std.mem.Allocator, self: *OpenDocument) void {
const default_size = basic_models.Size_f{ .w = 800, .h = 600 };
self.document = Document.init(default_size);
self.cpu_render = CpuRenderEngine.init(allocator, .Squares);
self.canvas = Canvas.init(
allocator,
&self.document,
(&self.cpu_render).renderEngine(),
);
self.selected_object_id = null;
initWithDocument(allocator, self, .init(.{
.w = 800,
.h = 600,
}));
}
pub fn initWithDocument(allocator: std.mem.Allocator, self: *OpenDocument, doc: Document) void {

View File

@@ -5,7 +5,10 @@ const WindowContext = @import("WindowContext.zig");
const ui = @import("ui/frame.zig");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
// std.heap.GeneralPurposeAllocator was renamed to DebugAllocator recently.
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var backend = try SDLBackend.initWindow(.{

View File

@@ -33,9 +33,9 @@ shape: ShapeKind,
properties: std.ArrayList(Property),
children: std.ArrayList(Object),
pub fn getProperty(self: Object, tag: std.meta.Tag(PropertyData)) ?*const PropertyData {
pub fn getProperty(self: Object, comptime tag: std.meta.Tag(PropertyData)) ?@FieldType(PropertyData, @tagName(tag)) {
for (self.properties.items) |*prop| {
if (std.meta.activeTag(prop.data) == tag) return &prop.data;
if (std.meta.activeTag(prop.data) == tag) return @field(prop.data, @tagName(tag));
}
return null;
}

View File

@@ -51,8 +51,7 @@ pub const Property = struct {
.points => |slice| .{
.data = .{
.points = blk: {
const copy = try allocator.alloc(Point2_f, slice.len);
@memcpy(copy, slice);
const copy = try allocator.dupe(Point2_f, slice);
break :blk copy;
},
},

View File

@@ -15,8 +15,7 @@ fn appendShapeProperties(allocator: std.mem.Allocator, obj: *Object, props: []co
for (props) |prop| {
if (prop.data == .points) {
const pts = prop.data.points;
const copy = try allocator.alloc(Point2_f, pts.len);
@memcpy(copy, pts);
const copy = try allocator.dupe(Point2_f, pts);
try obj.properties.append(allocator, .{ .data = .{ .points = copy } });
} else {
try obj.properties.append(allocator, prop);
@@ -33,7 +32,7 @@ pub fn createObject(allocator: std.mem.Allocator, shape_kind: Object.ShapeKind)
.ellipse => try appendShapeProperties(allocator, &obj, &ellipse.default_shape_properties),
.broken => {
try appendShapeProperties(allocator, &obj, &broken.default_shape_properties);
try obj.setProperty(allocator, .{ .data = .{ .fill_rgba = obj.getProperty(.stroke_rgba).?.stroke_rgba } });
try obj.setProperty(allocator, .{ .data = .{ .fill_rgba = obj.getProperty(.stroke_rgba).? } });
},
}
return obj;

View File

@@ -84,8 +84,8 @@ fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size
x += randFloat(rng, -40, 80);
y += randFloat(rng, -30, 60);
}
const slice = try allocator.alloc(Point2_f, list.items.len);
@memcpy(slice, list.items);
const slice = try allocator.dupe(Point2_f, list.items);
errdefer allocator.free(slice);
try obj.setProperty(allocator, .{ .data = .{ .points = slice } });
},
}
@@ -100,6 +100,7 @@ pub fn addRandomShapes(doc: *Document, allocator: std.mem.Allocator, rng: std.Ra
for (0..n_root) |_| {
if (total_count >= max_total) break;
var obj = try shape.createObject(allocator, randomShapeKind(rng));
defer obj.deinit(allocator);
try randomizeObjectProperties(allocator, &doc.size, &obj, rng);
try doc.addObject(allocator, obj);
total_count += 1;
@@ -117,6 +118,7 @@ pub fn addRandomShapes(doc: *Document, allocator: std.mem.Allocator, rng: std.Ra
for (0..n_children) |_| {
if (total_count >= max_total) break;
var child = try shape.createObject(allocator, randomShapeKind(rng));
defer child.deinit(allocator);
try randomizeObjectProperties(allocator, &doc.size, &child, rng);
try obj.addChild(allocator, child, &doc.next_object_id);
total_count += 1;

View File

@@ -66,9 +66,7 @@ fn renderGradient(self: CpuRenderEngine, pixels: []Color.PMA, width: u32, height
}
}
fn renderSquares(self: CpuRenderEngine, pixels: []Color.PMA, canvas_size: Size_i, visible_rect: Rect_i) void {
_ = self;
fn renderSquares(pixels: []Color.PMA, canvas_size: Size_i, visible_rect: Rect_i) void {
const colors = [_]Color.PMA{
.{ .r = 255, .g = 0, .b = 0, .a = 255 },
.{ .r = 255, .g = 165, .b = 0, .a = 255 },
@@ -169,10 +167,10 @@ pub fn example(self: CpuRenderEngine, canvas_size: Size_i, visible_rect: Rect_i)
switch (self.type) {
.Gradient => self.renderGradient(pixels, width, height, full_w, full_h, visible_rect),
.Squares => self.renderSquares(pixels, canvas_size, visible_rect),
.Squares => renderSquares(pixels, canvas_size, visible_rect),
}
return try dvui.textureCreate(pixels, width, height, .nearest);
return try dvui.textureCreate(pixels, width, height, .nearest, .rgba_8_8_8_8);
}
pub fn renderEngine(self: *CpuRenderEngine) RenderEngine {
@@ -191,7 +189,7 @@ pub fn renderDocument(self: *CpuRenderEngine, document: *const Document, canvas_
try cpu_draw.drawDocument(pixels, width, height, visible_rect, document, canvas_size, self._allocator);
self._renderStats.render_time_ns = t.read();
return try dvui.textureCreate(pixels, width, height, .nearest);
return try dvui.textureCreate(pixels, width, height, .nearest, .rgba_8_8_8_8);
}
pub fn getStats(self: CpuRenderEngine) RenderStats {

View File

@@ -16,14 +16,13 @@ pub fn draw(
obj: *const Object,
allocator: std.mem.Allocator,
) !void {
const p_prop = obj.getProperty(.points) orelse return;
const pts = p_prop.points;
const pts = obj.getProperty(.points) orelse return;
if (pts.len < 2) return;
const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke;
const thickness = if (obj.getProperty(.thickness)) |t| t.thickness else default_thickness;
const closed = if (obj.getProperty(.closed)) |c| c.closed else false;
const filled = if (obj.getProperty(.filled)) |f| f.filled else true;
const fill_color = if (obj.getProperty(.fill_rgba)) |f| pipeline.rgbaToPma(f.fill_rgba) else default_fill;
const stroke = if (obj.getProperty(.stroke_rgba)) |stroke_rgba| pipeline.rgbaToPma(stroke_rgba) else default_stroke;
const thickness = obj.getProperty(.thickness) orelse default_thickness;
const closed = obj.getProperty(.closed) orelse false;
const filled = obj.getProperty(.filled) orelse true;
const fill_color = if (obj.getProperty(.fill_rgba)) |fill_rgba| pipeline.rgbaToPma(fill_rgba) else default_fill;
const buffer = try allocator.alloc(Color.PMA, ctx.buf_width * ctx.buf_height);
@memset(buffer, .{ .r = 0, .g = 0, .b = 0, .a = 0 });

View File

@@ -13,7 +13,7 @@ const DrawContext = pipeline.DrawContext;
const Transform = pipeline.Transform;
fn isVisible(obj: *const Object) bool {
return if (obj.getProperty(.visible)) |p| p.visible else true;
return obj.getProperty(.visible) orelse true;
}
fn drawObject(

View File

@@ -17,17 +17,17 @@ const default_thickness: f32 = 2.0;
/// arc_percent: 100 — полный эллипс, иначе одна дуга; обход в коде от (0,ry) по квадрантам (визуально может казаться от низа против часовой из‑за экранной Y).
/// Отрисовка в отдельный буфер и один composite, чтобы при alpha<255 пиксели не накладывались несколько раз.
pub fn draw(ctx: *DrawContext, obj: *const Object, allocator: std.mem.Allocator) !void {
const r_prop = obj.getProperty(.radii) orelse return;
const rx = r_prop.radii.x;
const ry = r_prop.radii.y;
const radii = obj.getProperty(.radii) orelse return;
const rx = radii.x;
const ry = radii.y;
if (rx <= 0 or ry <= 0) return;
const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke;
const thickness = if (obj.getProperty(.thickness)) |t| t.thickness else default_thickness;
const arc_percent = std_math.clamp(if (obj.getProperty(.arc_percent)) |p| p.arc_percent else 100.0, 0.0, 100.0);
const closed = if (obj.getProperty(.closed)) |c| c.closed else true;
const filled = if (obj.getProperty(.filled)) |f| f.filled else false;
const fill_color = if (obj.getProperty(.fill_rgba)) |f| pipeline.rgbaToPma(f.fill_rgba) else default_fill;
const stroke = if (obj.getProperty(.stroke_rgba)) |stroke_rgba| pipeline.rgbaToPma(stroke_rgba) else default_stroke;
const thickness = if (obj.getProperty(.thickness)) |thickness| thickness else default_thickness;
const arc_percent = std_math.clamp(if (obj.getProperty(.arc_percent)) |arc_percent| arc_percent else 100.0, 0.0, 100.0);
const closed = obj.getProperty(.closed) orelse true;
const filled = obj.getProperty(.filled) orelse false;
const fill_color = if (obj.getProperty(.fill_rgba)) |fill_rgba| pipeline.rgbaToPma(fill_rgba) else default_fill;
const do_fill = filled and (closed or arc_percent >= 100.0);
const t = &ctx.transform;

View File

@@ -12,11 +12,11 @@ const default_thickness: f32 = 2.0;
/// Линия от (0,0) до end_point.
pub fn draw(ctx: *DrawContext, obj: *const Object) void {
const ep_prop = obj.getProperty(.end_point) orelse return;
const end_x = ep_prop.end_point.x;
const end_y = ep_prop.end_point.y;
const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke;
const thickness = if (obj.getProperty(.thickness)) |t| t.thickness else default_thickness;
const end_point = obj.getProperty(.end_point) orelse return;
const end_x = end_point.x;
const end_y = end_point.y;
const stroke = if (obj.getProperty(.stroke_rgba)) |stroke_rgba| pipeline.rgbaToPma(stroke_rgba) else default_stroke;
const thickness = obj.getProperty(.thickness) orelse default_thickness;
drawLine(ctx, 0, 0, end_x, end_y, stroke, thickness, false);
}

View File

@@ -16,10 +16,10 @@ pub const Transform = struct {
opacity: f32 = 1.0,
pub fn init(obj: *const Document.Object) Transform {
const pos = if (obj.getProperty(.position)) |p| p.position else Point2_f{ .x = 0, .y = 0 };
const angle = if (obj.getProperty(.angle)) |p| p.angle else 0;
const scale = if (obj.getProperty(.scale)) |p| p.scale else Scale2_f{ .scale_x = 1, .scale_y = 1 };
const opacity = if (obj.getProperty(.opacity)) |p| p.opacity else 1.0;
const pos = obj.getProperty(.position) orelse Point2_f{ .x = 0, .y = 0 };
const angle = obj.getProperty(.angle) orelse 0;
const scale = obj.getProperty(.scale) orelse Scale2_f{ .scale_x = 1, .scale_y = 1 };
const opacity = obj.getProperty(.opacity) orelse 1.0;
return .{
.position = pos,
.angle = angle,
@@ -273,7 +273,7 @@ const FillCanvas = struct {
}.lessThan);
// Семена: по строкам находим сегменты (пары x), пересекаем с окном буфера, берём середину сегмента.
var seeds = findFillSeeds(self, keys_buf.items, buf_w_i, buf_h_i, allocator) catch return;
var seeds = findFillSeeds(keys_buf.items, buf_w_i, buf_h_i, allocator) catch return;
defer seeds.deinit(allocator);
var stack = std.ArrayList(Point2_i).empty;
@@ -304,13 +304,11 @@ const FillCanvas = struct {
/// По строкам: рёбра (подряд идущие x) → сегменты между ними. Семена — середины чётных сегментов (при чётном числе границ).
fn findFillSeeds(
self: *const FillCanvas,
keys: []const Point2_i,
buf_w_i: i32,
buf_h_i: i32,
allocator: std.mem.Allocator,
) !std.ArrayList(Point2_i) {
_ = self;
var list = std.ArrayList(Point2_i).empty;
errdefer list.deinit(allocator);
var segments = std.ArrayList(struct { left: i32, right: i32 }).empty;

View File

@@ -17,14 +17,15 @@ pub fn canvasView(canvas: *Canvas, selected_object_id: ?u64, content_rect_scale:
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(),
.{
.scroll_info = &canvas.scroll,
.vertical_bar = .auto,
.horizontal_bar = .auto,
.process_events_after = false,
},
init_options,
.{
.expand = .both,
.background = false,
@@ -100,7 +101,7 @@ pub fn canvasView(canvas: *Canvas, selected_object_id: ?u64, content_rect_scale:
}
}
if (!scroll.init_opts.process_events_after) {
if (!init_options.process_events_after) {
if (scroll.scroll) |*sc| {
dvui.clipSet(sc.prevClip);
sc.processEventsAfter();
@@ -675,8 +676,7 @@ fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Prope
}
if (changed) {
const slice = canvas.allocator.alloc(Point2_f, list.items.len) catch return;
@memcpy(slice, list.items);
const slice = canvas.allocator.dupe(Point2_f, list.items) catch return;
obj.setProperty(canvas.allocator, .{ .data = .{ .points = slice } }) catch {
canvas.allocator.free(slice);
return;

View File

@@ -222,11 +222,13 @@ pub fn leftPanel(ctx: *WindowContext) void {
},
);
{
dvui.label(@src(), "Objects", .{}, .{ .font = .{
.id = dvui.themeGet().font_heading.id,
.line_height_factor = dvui.themeGet().font_heading.line_height_factor,
.size = dvui.themeGet().font_heading.size + 8,
}, .gravity_x = 0.5 });
dvui.label(@src(), "Objects", .{}, .{
.font = .{
.line_height_factor = dvui.themeGet().font_heading.line_height_factor,
.size = dvui.themeGet().font_heading.size + 8,
},
.gravity_x = 0.5,
});
var scroll = dvui.scrollArea(
@src(),
.{ .vertical = .auto, .horizontal = .auto },