json сохранение
This commit is contained in:
@@ -27,6 +27,17 @@ pub const OpenDocument = struct {
|
|||||||
self.selected_object_id = null;
|
self.selected_object_id = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn initWithDocument(allocator: std.mem.Allocator, self: *OpenDocument, doc: Document) void {
|
||||||
|
self.document = doc;
|
||||||
|
self.cpu_render = CpuRenderEngine.init(allocator, .Squares);
|
||||||
|
self.canvas = Canvas.init(
|
||||||
|
allocator,
|
||||||
|
&self.document,
|
||||||
|
(&self.cpu_render).renderEngine(),
|
||||||
|
);
|
||||||
|
self.selected_object_id = null;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *OpenDocument) void {
|
pub fn deinit(self: *OpenDocument) void {
|
||||||
self.document.deinit();
|
self.document.deinit();
|
||||||
self.canvas.deinit();
|
self.canvas.deinit();
|
||||||
@@ -73,6 +84,16 @@ pub fn addNewDocument(self: *WindowContext) !void {
|
|||||||
self.active_document_index = self.documents.items.len - 1;
|
self.active_document_index = self.documents.items.len - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn addDocument(self: *WindowContext, doc: Document) !void {
|
||||||
|
const ptr = try self.allocator.create(OpenDocument);
|
||||||
|
errdefer self.allocator.destroy(ptr);
|
||||||
|
var doc_mut = doc;
|
||||||
|
errdefer doc_mut.deinit();
|
||||||
|
OpenDocument.initWithDocument(self.allocator, ptr, doc_mut);
|
||||||
|
try self.documents.append(self.allocator, ptr);
|
||||||
|
self.active_document_index = self.documents.items.len - 1;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn setActiveDocument(self: *WindowContext, index: usize) void {
|
pub fn setActiveDocument(self: *WindowContext, index: usize) void {
|
||||||
if (index < self.documents.items.len) {
|
if (index < self.documents.items.len) {
|
||||||
self.active_document_index = index;
|
self.active_document_index = index;
|
||||||
|
|||||||
365
src/persistence/document_json.zig
Normal file
365
src/persistence/document_json.zig
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Document = @import("../models/Document.zig");
|
||||||
|
const Object = Document.Object;
|
||||||
|
const Property = @import("../models/Property.zig").Property;
|
||||||
|
const PropertyData = @import("../models/Property.zig").Data;
|
||||||
|
const basic_models = @import("../models/basic_models.zig");
|
||||||
|
const Point2_f = basic_models.Point2_f;
|
||||||
|
const Size_f = basic_models.Size_f;
|
||||||
|
const Scale2_f = basic_models.Scale2_f;
|
||||||
|
const Radii_f = basic_models.Radii_f;
|
||||||
|
|
||||||
|
pub const json_version: u32 = 1;
|
||||||
|
const JsonError = std.json.Stringify.Error;
|
||||||
|
|
||||||
|
pub fn saveToFile(doc: *const Document, path: []const u8) !void {
|
||||||
|
var file = try std.fs.cwd().createFile(path, .{ .truncate = true });
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
var buffer: [4096]u8 = undefined;
|
||||||
|
var writer = file.writer(&buffer);
|
||||||
|
var jw: std.json.Stringify = .{
|
||||||
|
.writer = &writer.interface,
|
||||||
|
.options = .{ .whitespace = .indent_2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
try writeDocument(&jw, doc);
|
||||||
|
try writer.interface.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loadFromFile(allocator: std.mem.Allocator, path: []const u8) !Document {
|
||||||
|
var file = try std.fs.cwd().openFile(path, .{});
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
const data = try file.readToEndAlloc(allocator, std.math.maxInt(usize));
|
||||||
|
defer allocator.free(data);
|
||||||
|
|
||||||
|
var parsed = try std.json.parseFromSlice(std.json.Value, allocator, data, .{ .parse_numbers = true });
|
||||||
|
defer parsed.deinit();
|
||||||
|
|
||||||
|
return try parseDocumentValue(allocator, parsed.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeDocument(jw: *std.json.Stringify, doc: *const Document) JsonError!void {
|
||||||
|
try jw.beginObject();
|
||||||
|
try jw.objectField("version");
|
||||||
|
try jw.write(json_version);
|
||||||
|
try jw.objectField("size");
|
||||||
|
try writeSize(jw, doc.size);
|
||||||
|
try jw.objectField("next_object_id");
|
||||||
|
try jw.write(doc.next_object_id);
|
||||||
|
try jw.objectField("objects");
|
||||||
|
try writeObjectArray(jw, doc.objects.items);
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeSize(jw: *std.json.Stringify, size: Size_f) JsonError!void {
|
||||||
|
try jw.beginObject();
|
||||||
|
try jw.objectField("w");
|
||||||
|
try jw.write(size.w);
|
||||||
|
try jw.objectField("h");
|
||||||
|
try jw.write(size.h);
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writePoint(jw: *std.json.Stringify, pt: Point2_f) JsonError!void {
|
||||||
|
try jw.beginObject();
|
||||||
|
try jw.objectField("x");
|
||||||
|
try jw.write(pt.x);
|
||||||
|
try jw.objectField("y");
|
||||||
|
try jw.write(pt.y);
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeScale(jw: *std.json.Stringify, scale: Scale2_f) JsonError!void {
|
||||||
|
try jw.beginObject();
|
||||||
|
try jw.objectField("scale_x");
|
||||||
|
try jw.write(scale.scale_x);
|
||||||
|
try jw.objectField("scale_y");
|
||||||
|
try jw.write(scale.scale_y);
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeRadii(jw: *std.json.Stringify, radii: Radii_f) JsonError!void {
|
||||||
|
try jw.beginObject();
|
||||||
|
try jw.objectField("x");
|
||||||
|
try jw.write(radii.x);
|
||||||
|
try jw.objectField("y");
|
||||||
|
try jw.write(radii.y);
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeObjectArray(jw: *std.json.Stringify, objects: []const Object) JsonError!void {
|
||||||
|
try jw.beginArray();
|
||||||
|
for (objects) |obj| {
|
||||||
|
try writeObject(jw, obj);
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeObject(jw: *std.json.Stringify, obj: Object) JsonError!void {
|
||||||
|
try jw.beginObject();
|
||||||
|
try jw.objectField("id");
|
||||||
|
try jw.write(obj.id);
|
||||||
|
try jw.objectField("shape");
|
||||||
|
try jw.write(@tagName(obj.shape));
|
||||||
|
try jw.objectField("properties");
|
||||||
|
try writeProperties(jw, obj.properties.items);
|
||||||
|
try jw.objectField("children");
|
||||||
|
try writeObjectArray(jw, obj.children.items);
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeProperties(jw: *std.json.Stringify, props: []const Property) JsonError!void {
|
||||||
|
try jw.beginArray();
|
||||||
|
for (props) |prop| {
|
||||||
|
try writeProperty(jw, prop);
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeProperty(jw: *std.json.Stringify, prop: Property) JsonError!void {
|
||||||
|
try jw.beginObject();
|
||||||
|
try jw.objectField("tag");
|
||||||
|
try jw.write(@tagName(prop.data));
|
||||||
|
try jw.objectField("value");
|
||||||
|
switch (prop.data) {
|
||||||
|
.position => |p| try writePoint(jw, p),
|
||||||
|
.angle => |v| try jw.write(v),
|
||||||
|
.scale => |s| try writeScale(jw, s),
|
||||||
|
.visible => |v| try jw.write(v),
|
||||||
|
.opacity => |v| try jw.write(v),
|
||||||
|
.locked => |v| try jw.write(v),
|
||||||
|
.size => |s| try writeSize(jw, s),
|
||||||
|
.radii => |r| try writeRadii(jw, r),
|
||||||
|
.end_point => |p| try writePoint(jw, p),
|
||||||
|
.points => |list| {
|
||||||
|
try jw.beginArray();
|
||||||
|
for (list.items) |pt| {
|
||||||
|
try writePoint(jw, pt);
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
},
|
||||||
|
.fill_rgba => |v| try jw.write(v),
|
||||||
|
.stroke_rgba => |v| try jw.write(v),
|
||||||
|
.thickness => |v| try jw.write(v),
|
||||||
|
}
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseDocumentValue(allocator: std.mem.Allocator, value: std.json.Value) !Document {
|
||||||
|
const obj = try expectObject(value);
|
||||||
|
const version_val = try getField(obj, "version");
|
||||||
|
const version = try valueToU32(version_val);
|
||||||
|
if (version != json_version) return error.UnsupportedVersion;
|
||||||
|
|
||||||
|
const size_val = try getField(obj, "size");
|
||||||
|
const size = try parseSize(size_val);
|
||||||
|
|
||||||
|
var document = Document{
|
||||||
|
.size = size,
|
||||||
|
.allocator = allocator,
|
||||||
|
.objects = std.ArrayList(Object).empty,
|
||||||
|
.next_object_id = 1,
|
||||||
|
};
|
||||||
|
errdefer document.deinit();
|
||||||
|
|
||||||
|
const objects_val = try getField(obj, "objects");
|
||||||
|
const objects_arr = try expectArray(objects_val);
|
||||||
|
for (objects_arr.items) |item| {
|
||||||
|
const obj_item = try parseObject(allocator, item);
|
||||||
|
try document.objects.append(allocator, obj_item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.get("next_object_id")) |next_val| {
|
||||||
|
const next_id = try valueToU64(next_val);
|
||||||
|
document.next_object_id = next_id;
|
||||||
|
} else {
|
||||||
|
document.next_object_id = computeNextObjectId(document.objects.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseObject(allocator: std.mem.Allocator, value: std.json.Value) !Object {
|
||||||
|
const obj = try expectObject(value);
|
||||||
|
const id = try valueToU64(try getField(obj, "id"));
|
||||||
|
const shape_str = try valueToString(try getField(obj, "shape"));
|
||||||
|
const shape = std.meta.stringToEnum(Object.ShapeKind, shape_str) orelse return error.InvalidJson;
|
||||||
|
|
||||||
|
var properties = std.ArrayList(Property).empty;
|
||||||
|
errdefer {
|
||||||
|
for (properties.items) |*p| p.deinit(allocator);
|
||||||
|
properties.deinit(allocator);
|
||||||
|
}
|
||||||
|
var children = std.ArrayList(Object).empty;
|
||||||
|
errdefer {
|
||||||
|
for (children.items) |*c| c.deinit(allocator);
|
||||||
|
children.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
const props_val = try getField(obj, "properties");
|
||||||
|
const props_arr = try expectArray(props_val);
|
||||||
|
for (props_arr.items) |item| {
|
||||||
|
try properties.append(allocator, try parseProperty(allocator, item));
|
||||||
|
}
|
||||||
|
|
||||||
|
const children_val = try getField(obj, "children");
|
||||||
|
const children_arr = try expectArray(children_val);
|
||||||
|
for (children_arr.items) |item| {
|
||||||
|
try children.append(allocator, try parseObject(allocator, item));
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.id = id,
|
||||||
|
.shape = shape,
|
||||||
|
.properties = properties,
|
||||||
|
.children = children,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseProperty(allocator: std.mem.Allocator, value: std.json.Value) !Property {
|
||||||
|
const obj = try expectObject(value);
|
||||||
|
const tag = try valueToString(try getField(obj, "tag"));
|
||||||
|
const val = try getField(obj, "value");
|
||||||
|
|
||||||
|
const data: PropertyData = if (std.mem.eql(u8, tag, "position")) blk: {
|
||||||
|
break :blk .{ .position = try parsePoint(val) };
|
||||||
|
} else if (std.mem.eql(u8, tag, "angle")) blk: {
|
||||||
|
break :blk .{ .angle = try valueToF32(val) };
|
||||||
|
} else if (std.mem.eql(u8, tag, "scale")) blk: {
|
||||||
|
break :blk .{ .scale = try parseScale(val) };
|
||||||
|
} else if (std.mem.eql(u8, tag, "visible")) blk: {
|
||||||
|
break :blk .{ .visible = try valueToBool(val) };
|
||||||
|
} else if (std.mem.eql(u8, tag, "opacity")) blk: {
|
||||||
|
break :blk .{ .opacity = try valueToF32(val) };
|
||||||
|
} else if (std.mem.eql(u8, tag, "locked")) blk: {
|
||||||
|
break :blk .{ .locked = try valueToBool(val) };
|
||||||
|
} else if (std.mem.eql(u8, tag, "size")) blk: {
|
||||||
|
break :blk .{ .size = try parseSize(val) };
|
||||||
|
} else if (std.mem.eql(u8, tag, "radii")) blk: {
|
||||||
|
break :blk .{ .radii = try parseRadii(val) };
|
||||||
|
} else if (std.mem.eql(u8, tag, "end_point")) blk: {
|
||||||
|
break :blk .{ .end_point = try parsePoint(val) };
|
||||||
|
} else if (std.mem.eql(u8, tag, "points")) blk: {
|
||||||
|
const arr = try expectArray(val);
|
||||||
|
var list = std.ArrayList(Point2_f).empty;
|
||||||
|
errdefer list.deinit(allocator);
|
||||||
|
for (arr.items) |item| {
|
||||||
|
try list.append(allocator, try parsePoint(item));
|
||||||
|
}
|
||||||
|
break :blk .{ .points = list };
|
||||||
|
} else if (std.mem.eql(u8, tag, "fill_rgba")) blk: {
|
||||||
|
break :blk .{ .fill_rgba = try valueToU32(val) };
|
||||||
|
} else if (std.mem.eql(u8, tag, "stroke_rgba")) blk: {
|
||||||
|
break :blk .{ .stroke_rgba = try valueToU32(val) };
|
||||||
|
} else if (std.mem.eql(u8, tag, "thickness")) blk: {
|
||||||
|
break :blk .{ .thickness = try valueToF32(val) };
|
||||||
|
} else {
|
||||||
|
return error.InvalidJson;
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{ .data = data };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parsePoint(value: std.json.Value) !Point2_f {
|
||||||
|
const obj = try expectObject(value);
|
||||||
|
const x = try valueToF32(try getField(obj, "x"));
|
||||||
|
const y = try valueToF32(try getField(obj, "y"));
|
||||||
|
return .{ .x = x, .y = y };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseSize(value: std.json.Value) !Size_f {
|
||||||
|
const obj = try expectObject(value);
|
||||||
|
const w = try valueToF32(try getField(obj, "w"));
|
||||||
|
const h = try valueToF32(try getField(obj, "h"));
|
||||||
|
return .{ .w = w, .h = h };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseScale(value: std.json.Value) !Scale2_f {
|
||||||
|
const obj = try expectObject(value);
|
||||||
|
const sx = try valueToF32(try getField(obj, "scale_x"));
|
||||||
|
const sy = try valueToF32(try getField(obj, "scale_y"));
|
||||||
|
return .{ .scale_x = sx, .scale_y = sy };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseRadii(value: std.json.Value) !Radii_f {
|
||||||
|
const obj = try expectObject(value);
|
||||||
|
const x = try valueToF32(try getField(obj, "x"));
|
||||||
|
const y = try valueToF32(try getField(obj, "y"));
|
||||||
|
return .{ .x = x, .y = y };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expectObject(value: std.json.Value) !std.json.ObjectMap {
|
||||||
|
return switch (value) {
|
||||||
|
.object => |o| o,
|
||||||
|
else => error.InvalidJson,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expectArray(value: std.json.Value) !std.json.Array {
|
||||||
|
return switch (value) {
|
||||||
|
.array => |a| a,
|
||||||
|
else => error.InvalidJson,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getField(obj: std.json.ObjectMap, key: []const u8) !std.json.Value {
|
||||||
|
return obj.get(key) orelse error.InvalidJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valueToString(value: std.json.Value) ![]const u8 {
|
||||||
|
return switch (value) {
|
||||||
|
.string => |s| s,
|
||||||
|
else => error.InvalidJson,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valueToBool(value: std.json.Value) !bool {
|
||||||
|
return switch (value) {
|
||||||
|
.bool => |b| b,
|
||||||
|
else => error.InvalidJson,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valueToF32(value: std.json.Value) !f32 {
|
||||||
|
return switch (value) {
|
||||||
|
.float => |f| @floatCast(f),
|
||||||
|
.integer => |i| @floatFromInt(i),
|
||||||
|
else => error.InvalidJson,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valueToU32(value: std.json.Value) !u32 {
|
||||||
|
const v = switch (value) {
|
||||||
|
.integer => |i| i,
|
||||||
|
else => return error.InvalidJson,
|
||||||
|
};
|
||||||
|
if (v < 0 or v > std.math.maxInt(u32)) return error.InvalidJson;
|
||||||
|
return @intCast(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valueToU64(value: std.json.Value) !u64 {
|
||||||
|
const v = switch (value) {
|
||||||
|
.integer => |i| i,
|
||||||
|
else => return error.InvalidJson,
|
||||||
|
};
|
||||||
|
if (v < 0) return error.InvalidJson;
|
||||||
|
return @intCast(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn computeNextObjectId(objects: []const Object) u64 {
|
||||||
|
var max_id: u64 = 0;
|
||||||
|
for (objects) |obj| {
|
||||||
|
max_id = @max(max_id, maxIdInObject(obj));
|
||||||
|
}
|
||||||
|
return max_id + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maxIdInObject(obj: Object) u64 {
|
||||||
|
var max_id = obj.id;
|
||||||
|
for (obj.children.items) |child| {
|
||||||
|
max_id = @max(max_id, maxIdInObject(child));
|
||||||
|
}
|
||||||
|
return max_id;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
const dvui = @import("dvui");
|
const dvui = @import("dvui");
|
||||||
const WindowContext = @import("../WindowContext.zig");
|
const WindowContext = @import("../WindowContext.zig");
|
||||||
|
const menu_bar = @import("menu_bar.zig");
|
||||||
const tab_bar = @import("tab_bar.zig");
|
const tab_bar = @import("tab_bar.zig");
|
||||||
const left_panel = @import("left_panel.zig");
|
const left_panel = @import("left_panel.zig");
|
||||||
const right_panel = @import("right_panel.zig");
|
const right_panel = @import("right_panel.zig");
|
||||||
@@ -16,6 +17,7 @@ pub fn guiFrame(ctx: *WindowContext) bool {
|
|||||||
.{ .expand = .both, .background = true, .style = .window },
|
.{ .expand = .both, .background = true, .style = .window },
|
||||||
);
|
);
|
||||||
{
|
{
|
||||||
|
menu_bar.menuBar(ctx);
|
||||||
tab_bar.tabBar(ctx);
|
tab_bar.tabBar(ctx);
|
||||||
|
|
||||||
var content_row = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .both });
|
var content_row = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .both });
|
||||||
|
|||||||
89
src/ui/menu_bar.zig
Normal file
89
src/ui/menu_bar.zig
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const dvui = @import("dvui");
|
||||||
|
const WindowContext = @import("../WindowContext.zig");
|
||||||
|
const document_json = @import("../persistence/document_json.zig");
|
||||||
|
|
||||||
|
pub fn menuBar(ctx: *WindowContext) void {
|
||||||
|
var m = dvui.menu(@src(), .horizontal, .{ .background = true, .expand = .horizontal });
|
||||||
|
defer m.deinit();
|
||||||
|
|
||||||
|
if (dvui.menuItemLabel(@src(), "File", .{ .submenu = true }, .{ .expand = .none })) |r| {
|
||||||
|
var fm = dvui.floatingMenu(@src(), .{ .from = r }, .{});
|
||||||
|
defer fm.deinit();
|
||||||
|
|
||||||
|
if (dvui.menuItemLabel(@src(), "Open", .{}, .{ .expand = .horizontal }) != null) {
|
||||||
|
m.close();
|
||||||
|
openDocumentDialog(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dvui.menuItemLabel(@src(), "Save As", .{}, .{ .expand = .horizontal }) != null) {
|
||||||
|
m.close();
|
||||||
|
saveAsDialog(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openDocumentDialog(ctx: *WindowContext) void {
|
||||||
|
const path_z = dvui.dialogNativeFileOpen(ctx.allocator, .{
|
||||||
|
.title = "Open",
|
||||||
|
.filters = &.{ "*.json" },
|
||||||
|
.filter_description = "JSON files",
|
||||||
|
}) catch |err| {
|
||||||
|
std.debug.print("Open dialog error: {}\n", .{err});
|
||||||
|
return;
|
||||||
|
} orelse return;
|
||||||
|
defer ctx.allocator.free(path_z);
|
||||||
|
|
||||||
|
const path = path_z[0..path_z.len];
|
||||||
|
const doc = document_json.loadFromFile(ctx.allocator, path) catch |err| {
|
||||||
|
std.debug.print("Open file error: {}\n", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
ctx.addDocument(doc) catch |err| {
|
||||||
|
std.debug.print("Add document error: {}\n", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn saveAsDialog(ctx: *WindowContext) void {
|
||||||
|
const open_doc = ctx.activeDocument() orelse return;
|
||||||
|
const path_z = dvui.dialogNativeFileSave(ctx.allocator, .{
|
||||||
|
.title = "Save As",
|
||||||
|
.filters = &.{ "*.json" },
|
||||||
|
.filter_description = "JSON files",
|
||||||
|
}) catch |err| {
|
||||||
|
std.debug.print("Save dialog error: {}\n", .{err});
|
||||||
|
return;
|
||||||
|
} orelse return;
|
||||||
|
defer ctx.allocator.free(path_z);
|
||||||
|
|
||||||
|
const path_raw = path_z[0..path_z.len];
|
||||||
|
const resolved = ensureJsonPath(ctx.allocator, path_raw) catch |err| {
|
||||||
|
std.debug.print("Save path error: {}\n", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer if (resolved.owned) ctx.allocator.free(resolved.path);
|
||||||
|
|
||||||
|
document_json.saveToFile(&open_doc.document, resolved.path) catch |err| {
|
||||||
|
std.debug.print("Save file error: {}\n", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResolvedPath = struct {
|
||||||
|
path: []const u8,
|
||||||
|
owned: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn ensureJsonPath(allocator: std.mem.Allocator, path: []const u8) !ResolvedPath {
|
||||||
|
if (endsWithIgnoreCase(path, ".json")) {
|
||||||
|
return .{ .path = path, .owned = false };
|
||||||
|
}
|
||||||
|
const joined = try std.mem.concat(allocator, u8, &.{ path, ".json" });
|
||||||
|
return .{ .path = joined, .owned = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endsWithIgnoreCase(haystack: []const u8, needle: []const u8) bool {
|
||||||
|
if (haystack.len < needle.len) return false;
|
||||||
|
return std.ascii.eqlIgnoreCase(haystack[haystack.len - needle.len ..], needle);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user