Вкладки документов
This commit is contained in:
310
src/main.zig
310
src/main.zig
@@ -3,7 +3,6 @@ const builtin = @import("builtin");
|
||||
const dvui = @import("dvui");
|
||||
const dvui_ext = @import("ui/dvui_ext.zig");
|
||||
const SDLBackend = @import("sdl-backend");
|
||||
const Document = @import("models/Document.zig");
|
||||
const ImageRect = @import("models/basic_models.zig").ImageRect;
|
||||
const WindowContext = @import("WindowContext.zig");
|
||||
const sdl_c = SDLBackend.c;
|
||||
@@ -70,8 +69,8 @@ pub fn main() !void {
|
||||
}
|
||||
|
||||
fn gui_frame(ctx: *WindowContext) bool {
|
||||
const canvas = &ctx.canvas;
|
||||
const ctrl: bool = dvui.currentWindow().modifiers.control();
|
||||
const active_doc = ctx.activeDocument();
|
||||
|
||||
for (dvui.events()) |*e| {
|
||||
if (e.evt == .window and e.evt.window.action == .close) return false;
|
||||
@@ -80,172 +79,211 @@ fn gui_frame(ctx: *WindowContext) bool {
|
||||
|
||||
const root = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .horizontal },
|
||||
.{ .dir = .vertical },
|
||||
.{ .expand = .both, .background = true, .style = .window },
|
||||
);
|
||||
defer root.deinit();
|
||||
|
||||
// Левая панель с фиксированной шириной
|
||||
var left_panel = dvui.box(@src(), .{ .dir = .vertical }, .{ .expand = .vertical, .min_size_content = .{ .w = 200 }, .background = true });
|
||||
// Верхняя строка: таббар (вкладки документов + кнопка "Новый")
|
||||
var tab_bar = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal, .min_size_content = .{ .h = 32 }, .background = true, .padding = dvui.Rect.all(4) });
|
||||
{
|
||||
dvui.label(@src(), "Tools", .{}, .{});
|
||||
if (dvui.button(@src(), "Fill Random Color", .{}, .{}) or ctx.frame_index == 0) {
|
||||
canvas.exampleReset() catch |err| {
|
||||
std.debug.print("Error reset example: {}\n", .{err});
|
||||
};
|
||||
canvas.pos = .{ .x = 400, .y = 400 };
|
||||
}
|
||||
if (dvui.checkbox(@src(), &canvas.native_scaling, "Scaling", .{})) {}
|
||||
if (dvui.button(@src(), if (ctx.cpu_render.type == .Gradient) "Gradient" else "Squares", .{}, .{})) {
|
||||
if (ctx.cpu_render.type == .Gradient) {
|
||||
ctx.cpu_render.type = .Squares;
|
||||
} else {
|
||||
ctx.cpu_render.type = .Gradient;
|
||||
for (ctx.documents.items, 0..) |open_doc, i| {
|
||||
_ = open_doc;
|
||||
var buf: [32]u8 = undefined;
|
||||
const label = std.fmt.bufPrint(&buf, "Doc {d}", .{i + 1}) catch "Doc";
|
||||
if (dvui.button(@src(), label, .{}, .{ .id_extra = i })) {
|
||||
ctx.setActiveDocument(i);
|
||||
}
|
||||
canvas.redrawExample() catch {};
|
||||
}
|
||||
if (dvui.button(@src(), "+", .{}, .{})) {
|
||||
ctx.addNewDocument() catch |err| {
|
||||
std.debug.print("addNewDocument error: {}\n", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
left_panel.deinit();
|
||||
tab_bar.deinit();
|
||||
|
||||
// Правая панель - занимает оставшееся пространство
|
||||
const back = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .horizontal },
|
||||
.{ .expand = .both, .padding = dvui.Rect.all(12), .background = true },
|
||||
);
|
||||
// Нижняя строка: левая панель + контент
|
||||
var content_row = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .both }); // no background to not override
|
||||
{
|
||||
const fill_color = Color.black.opacity(0.25);
|
||||
var right_panel = dvui.box(
|
||||
// Левая панель с фиксированной шириной (инструменты для активного документа)
|
||||
var left_panel = dvui.box(@src(), .{ .dir = .vertical }, .{ .expand = .vertical, .min_size_content = .{ .w = 200 }, .background = true });
|
||||
{
|
||||
dvui.label(@src(), "Tools", .{}, .{});
|
||||
if (active_doc) |doc| {
|
||||
const canvas = &doc.canvas;
|
||||
if (dvui.button(@src(), "Fill Random Color", .{}, .{}) or ctx.frame_index == 0) {
|
||||
canvas.exampleReset() catch |err| {
|
||||
std.debug.print("Error reset example: {}\n", .{err});
|
||||
};
|
||||
canvas.pos = .{ .x = 400, .y = 400 };
|
||||
}
|
||||
if (dvui.checkbox(@src(), &canvas.native_scaling, "Scaling", .{})) {}
|
||||
if (dvui.button(@src(), if (doc.cpu_render.type == .Gradient) "Gradient" else "Squares", .{}, .{})) {
|
||||
if (doc.cpu_render.type == .Gradient) {
|
||||
doc.cpu_render.type = .Squares;
|
||||
} else {
|
||||
doc.cpu_render.type = .Gradient;
|
||||
}
|
||||
canvas.redrawExample() catch {};
|
||||
}
|
||||
} else {
|
||||
dvui.label(@src(), "No document", .{}, .{});
|
||||
}
|
||||
}
|
||||
left_panel.deinit();
|
||||
|
||||
// Правая панель — контент выбранного документа или заглушка
|
||||
const back = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .vertical },
|
||||
.{
|
||||
.expand = .both,
|
||||
.background = true,
|
||||
.padding = dvui.Rect.all(5),
|
||||
.corner_radius = dvui.Rect.all(24),
|
||||
.color_fill = fill_color,
|
||||
},
|
||||
.{ .dir = .horizontal },
|
||||
.{ .expand = .both, .padding = dvui.Rect.all(12), .background = true },
|
||||
);
|
||||
{
|
||||
var textured = dvui_ext.texturedBox(right_panel.data().contentRectScale(), dvui.Rect.all(20));
|
||||
const fill_color = Color.black.opacity(0.25);
|
||||
var right_panel = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .vertical },
|
||||
.{
|
||||
.expand = .both,
|
||||
.background = true,
|
||||
.padding = dvui.Rect.all(5),
|
||||
.corner_radius = dvui.Rect.all(24),
|
||||
.color_fill = fill_color,
|
||||
},
|
||||
);
|
||||
{
|
||||
var overlay = dvui.overlay(
|
||||
@src(),
|
||||
.{ .expand = .both },
|
||||
);
|
||||
{
|
||||
var scroll = dvui.scrollArea(
|
||||
@src(),
|
||||
.{
|
||||
.scroll_info = &canvas.scroll,
|
||||
.vertical_bar = .auto,
|
||||
.horizontal_bar = .auto,
|
||||
},
|
||||
.{
|
||||
.expand = .both,
|
||||
.background = false,
|
||||
},
|
||||
);
|
||||
if (active_doc) |doc| {
|
||||
const canvas = &doc.canvas;
|
||||
var textured = dvui_ext.texturedBox(right_panel.data().contentRectScale(), dvui.Rect.all(20));
|
||||
{
|
||||
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
|
||||
const img_size = canvas.getScaledImageSize();
|
||||
|
||||
// Получить viewport и scroll offset
|
||||
const viewport_rect = scroll.data().contentRect();
|
||||
const scroll_current = dvui.Point{ .x = canvas.scroll.viewport.x, .y = canvas.scroll.viewport.y };
|
||||
|
||||
// viewport_rect/scroll_current — в natural единицах.
|
||||
// Для расчёта видимой области в пикселях изображения переводим в physical.
|
||||
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,
|
||||
};
|
||||
|
||||
canvas.updateVisibleImageRect(viewport_px, scroll_px) catch |err| {
|
||||
std.debug.print("updateVisibleImageRect error: {}\n", .{err});
|
||||
};
|
||||
|
||||
// `canvas.texture` contains ONLY the visible part.
|
||||
// If we render it inside a widget sized as the full image, dvui will stretch it.
|
||||
// Instead: create a scroll content surface sized like the full image, then place
|
||||
// the visible texture at the correct offset at 1:1.
|
||||
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(
|
||||
var overlay = dvui.overlay(
|
||||
@src(),
|
||||
.{ .min_size_content = .{ .w = content_w, .h = content_h }, .background = false },
|
||||
.{ .expand = .both },
|
||||
);
|
||||
{
|
||||
if (canvas.texture) |tex| {
|
||||
const vis = canvas._visible_rect orelse ImageRect{ .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;
|
||||
var scroll = dvui.scrollArea(
|
||||
@src(),
|
||||
.{
|
||||
.scroll_info = &canvas.scroll,
|
||||
.vertical_bar = .auto,
|
||||
.horizontal_bar = .auto,
|
||||
},
|
||||
.{
|
||||
.expand = .both,
|
||||
.background = false,
|
||||
},
|
||||
);
|
||||
{
|
||||
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
|
||||
const img_size = canvas.getScaledImageSize();
|
||||
|
||||
_ = dvui.image(
|
||||
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,
|
||||
};
|
||||
|
||||
canvas.updateVisibleImageRect(viewport_px, scroll_px) catch |err| {
|
||||
std.debug.print("updateVisibleImageRect 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(),
|
||||
.{ .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,
|
||||
},
|
||||
},
|
||||
.{ .min_size_content = .{ .w = content_w, .h = content_h }, .background = false },
|
||||
);
|
||||
}
|
||||
}
|
||||
canvas_layer.deinit();
|
||||
{
|
||||
if (canvas.texture) |tex| {
|
||||
const vis = canvas._visible_rect orelse ImageRect{ .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;
|
||||
|
||||
// Заблокировать события скролла, если нажат ctrl
|
||||
if (ctrl) {
|
||||
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| {
|
||||
canvas.addZoom(y / 1000);
|
||||
canvas.redrawExample() catch {};
|
||||
_ = 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,
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
e.handled = true;
|
||||
.max_size_content = .{
|
||||
.w = @as(f32, @floatFromInt(vis.w)) / natural_scale,
|
||||
.h = @as(f32, @floatFromInt(vis.h)) / natural_scale,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
canvas_layer.deinit();
|
||||
|
||||
if (ctrl) {
|
||||
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| {
|
||||
canvas.addZoom(y / 1000);
|
||||
canvas.redrawExample() catch {};
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
e.handled = true;
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
scroll.deinit();
|
||||
|
||||
dvui.label(@src(), "Canvas", .{}, .{ .gravity_x = 0.5, .gravity_y = 0.0 });
|
||||
}
|
||||
overlay.deinit();
|
||||
}
|
||||
textured.deinit();
|
||||
} else {
|
||||
var no_doc_center = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .vertical },
|
||||
.{ .expand = .both, .padding = dvui.Rect.all(20) },
|
||||
);
|
||||
{
|
||||
dvui.label(@src(), "No document open", .{}, .{});
|
||||
if (dvui.button(@src(), "New document", .{}, .{})) {
|
||||
ctx.addNewDocument() catch |err| {
|
||||
std.debug.print("addNewDocument error: {}\n", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
scroll.deinit();
|
||||
|
||||
dvui.label(@src(), "Canvas", .{}, .{ .gravity_x = 0.5, .gravity_y = 0.0 });
|
||||
no_doc_center.deinit();
|
||||
}
|
||||
overlay.deinit();
|
||||
}
|
||||
textured.deinit();
|
||||
right_panel.deinit();
|
||||
}
|
||||
right_panel.deinit();
|
||||
back.deinit();
|
||||
}
|
||||
back.deinit();
|
||||
content_row.deinit();
|
||||
|
||||
ctx.frame_index += 1;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user