Refactor: Разделил UI на модули
Разделил главный фрейм UI на отдельные модули: tab_bar, left_panel, right_panel и canvas_view. Это улучшает читаемость и поддерживает принцип единственной ответственности. Также изменил функцию `updateVisibleImageRect`, чтобы она возвращала `bool`, указывающий на необходимость перерисовки.
This commit is contained in:
@@ -117,7 +117,7 @@ pub fn getScaledImageSize(self: Canvas) ImageRect {
|
||||
/// `viewport` и `scroll_offset` ожидаются в *physical* пикселях (т.е. уже умноженные на windowNaturalScale).
|
||||
///
|
||||
/// После обновления (или если текстуры ещё нет) перерисовывает текстуру, чтобы она содержала только видимую часть.
|
||||
pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) !void {
|
||||
pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) !bool {
|
||||
const next = computeVisibleImageRect(self.*, viewport, scroll_offset);
|
||||
var changed = false;
|
||||
if (self._visible_rect) |vis| {
|
||||
@@ -128,8 +128,9 @@ pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset:
|
||||
std.debug.print("Visible Image Rect: {{ x: {}, y: {}, w: {}, h: {} }}\n", .{ next.x, next.y, next.w, next.h });
|
||||
}
|
||||
if (changed or self.texture == null) {
|
||||
requestRedraw(self);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn computeVisibleImageRect(self: Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) ImageRect {
|
||||
|
||||
243
src/main.zig
243
src/main.zig
@@ -1,13 +1,8 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const dvui = @import("dvui");
|
||||
const dvui_ext = @import("ui/dvui_ext.zig");
|
||||
const SDLBackend = @import("sdl-backend");
|
||||
const ImageRect = @import("models/basic_models.zig").ImageRect;
|
||||
const WindowContext = @import("WindowContext.zig");
|
||||
const sdl_c = SDLBackend.c;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Color = dvui.Color;
|
||||
const ui = @import("ui/frame.zig");
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
@@ -34,255 +29,21 @@ pub fn main() !void {
|
||||
|
||||
var interrupted = false;
|
||||
main_loop: while (true) {
|
||||
// beginWait coordinates with waitTime below to run frames only when needed
|
||||
const nstime = win.beginWait(interrupted);
|
||||
|
||||
// marks the beginning of a frame for dvui, can call dvui functions after this
|
||||
try win.begin(nstime);
|
||||
|
||||
// send all SDL events to dvui for processing
|
||||
try backend.addAllEvents(&win);
|
||||
|
||||
// if dvui widgets might not cover the whole window, then need to clear
|
||||
// the previous frame's render
|
||||
_ = SDLBackend.c.SDL_SetRenderDrawColor(backend.renderer, 0, 0, 0, 255);
|
||||
_ = SDLBackend.c.SDL_RenderClear(backend.renderer);
|
||||
|
||||
const keep_running = gui_frame(&ctx);
|
||||
if (!keep_running) break :main_loop;
|
||||
if (!ui.guiFrame(&ctx)) break :main_loop;
|
||||
|
||||
// marks end of dvui frame, don't call dvui functions after this
|
||||
// - sends all dvui stuff to backend for rendering, must be called before renderPresent()
|
||||
const end_micros = try win.end(.{});
|
||||
|
||||
// cursor management
|
||||
try backend.setCursor(win.cursorRequested());
|
||||
try backend.textInputRect(win.textInputRequested());
|
||||
|
||||
// render frame to OS
|
||||
try backend.renderPresent();
|
||||
|
||||
// waitTime and beginWait combine to achieve variable framerates
|
||||
const wait_event_micros = win.waitTime(end_micros);
|
||||
interrupted = try backend.waitEventTimeout(wait_event_micros);
|
||||
}
|
||||
}
|
||||
|
||||
fn gui_frame(ctx: *WindowContext) bool {
|
||||
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;
|
||||
if (e.evt == .app and e.evt.app.action == .quit) return false;
|
||||
}
|
||||
|
||||
const root = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .vertical },
|
||||
.{ .expand = .both, .background = true, .style = .window },
|
||||
);
|
||||
defer root.deinit();
|
||||
|
||||
// Верхняя строка: таббар (вкладки документов + кнопка "Новый")
|
||||
var tab_bar = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal, .min_size_content = .{ .h = 32 }, .background = true, .padding = dvui.Rect.all(4) });
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (dvui.button(@src(), "+", .{}, .{})) {
|
||||
ctx.addNewDocument() catch |err| {
|
||||
std.debug.print("addNewDocument error: {}\n", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
tab_bar.deinit();
|
||||
|
||||
// Нижняя строка: левая панель + контент
|
||||
var content_row = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .both }); // no background to not override
|
||||
{
|
||||
// Левая панель с фиксированной шириной (инструменты для активного документа)
|
||||
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.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 = .horizontal },
|
||||
.{ .expand = .both, .padding = dvui.Rect.all(12), .background = true },
|
||||
);
|
||||
{
|
||||
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,
|
||||
},
|
||||
);
|
||||
{
|
||||
if (active_doc) |doc| {
|
||||
const canvas = &doc.canvas;
|
||||
var textured = dvui_ext.texturedBox(right_panel.data().contentRectScale(), dvui.Rect.all(20));
|
||||
{
|
||||
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,
|
||||
},
|
||||
);
|
||||
{
|
||||
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
|
||||
const img_size = canvas.getScaledImageSize();
|
||||
|
||||
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});
|
||||
};
|
||||
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 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;
|
||||
|
||||
_ = 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();
|
||||
|
||||
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.requestRedraw();
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
e.handled = true;
|
||||
}
|
||||
},
|
||||
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});
|
||||
};
|
||||
}
|
||||
}
|
||||
no_doc_center.deinit();
|
||||
}
|
||||
}
|
||||
right_panel.deinit();
|
||||
}
|
||||
back.deinit();
|
||||
}
|
||||
content_row.deinit();
|
||||
|
||||
ctx.frame_index += 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
122
src/ui/canvas_view.zig
Normal file
122
src/ui/canvas_view.zig
Normal file
@@ -0,0 +1,122 @@
|
||||
// Виджет холста: скролл, текстура, зум по Ctrl+колёсико.
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
const dvui_ext = @import("dvui_ext.zig");
|
||||
const Canvas = @import("../Canvas.zig");
|
||||
const ImageRect = @import("../models/basic_models.zig").ImageRect;
|
||||
|
||||
pub fn canvasView(canvas: *Canvas, 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 });
|
||||
{
|
||||
var scroll = dvui.scrollArea(
|
||||
@src(),
|
||||
.{
|
||||
.scroll_info = &canvas.scroll,
|
||||
.vertical_bar = .auto,
|
||||
.horizontal_bar = .auto,
|
||||
},
|
||||
.{ .expand = .both, .background = false },
|
||||
);
|
||||
{
|
||||
drawCanvasContent(canvas, scroll);
|
||||
handleCanvasZoom(canvas, scroll);
|
||||
}
|
||||
scroll.deinit();
|
||||
|
||||
dvui.label(@src(), "Canvas", .{}, .{ .gravity_x = 0.5, .gravity_y = 0.0 });
|
||||
}
|
||||
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.getScaledImageSize();
|
||||
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) catch |err| {
|
||||
std.debug.print("updateVisibleImageRect error: {}\n", .{err});
|
||||
return false;
|
||||
};
|
||||
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 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;
|
||||
|
||||
_ = 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;
|
||||
|
||||
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.requestRedraw();
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
e.handled = true;
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/ui/frame.zig
Normal file
43
src/ui/frame.zig
Normal file
@@ -0,0 +1,43 @@
|
||||
// Корневой кадр UI: разметка и сборка панелей.
|
||||
const dvui = @import("dvui");
|
||||
const WindowContext = @import("../WindowContext.zig");
|
||||
const tab_bar = @import("tab_bar.zig");
|
||||
const left_panel = @import("left_panel.zig");
|
||||
const right_panel = @import("right_panel.zig");
|
||||
|
||||
/// Отрисовать один кадр GUI. Возвращает false при закрытии окна/выходе.
|
||||
pub fn guiFrame(ctx: *WindowContext) bool {
|
||||
for (dvui.events()) |*e| {
|
||||
if (e.evt == .window and e.evt.window.action == .close) return false;
|
||||
if (e.evt == .app and e.evt.app.action == .quit) return false;
|
||||
}
|
||||
|
||||
var root = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .vertical },
|
||||
.{ .expand = .both, .background = true, .style = .window },
|
||||
);
|
||||
{
|
||||
tab_bar.tabBar(ctx);
|
||||
|
||||
var content_row = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .both });
|
||||
{
|
||||
left_panel.leftPanel(ctx);
|
||||
|
||||
var back = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .horizontal },
|
||||
.{ .expand = .both, .padding = dvui.Rect.all(12), .background = true },
|
||||
);
|
||||
{
|
||||
right_panel.rightPanel(ctx);
|
||||
}
|
||||
back.deinit();
|
||||
}
|
||||
content_row.deinit();
|
||||
}
|
||||
root.deinit();
|
||||
|
||||
ctx.frame_index += 1;
|
||||
return true;
|
||||
}
|
||||
31
src/ui/left_panel.zig
Normal file
31
src/ui/left_panel.zig
Normal file
@@ -0,0 +1,31 @@
|
||||
// Левая панель: инструменты для активного документа (scaling, тип рендера).
|
||||
const dvui = @import("dvui");
|
||||
const WindowContext = @import("../WindowContext.zig");
|
||||
|
||||
pub fn leftPanel(ctx: *WindowContext) void {
|
||||
var panel = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .vertical },
|
||||
.{ .expand = .vertical, .min_size_content = .{ .w = 200 }, .background = true },
|
||||
);
|
||||
{
|
||||
dvui.label(@src(), "Tools", .{}, .{});
|
||||
|
||||
const active_doc = ctx.activeDocument();
|
||||
if (active_doc) |doc| {
|
||||
const canvas = &doc.canvas;
|
||||
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", .{}, .{});
|
||||
}
|
||||
}
|
||||
panel.deinit();
|
||||
}
|
||||
47
src/ui/right_panel.zig
Normal file
47
src/ui/right_panel.zig
Normal file
@@ -0,0 +1,47 @@
|
||||
// Правая панель: контент документа (холст) или заглушка «Нет документа».
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
const WindowContext = @import("../WindowContext.zig");
|
||||
const canvas_view = @import("canvas_view.zig");
|
||||
|
||||
pub fn rightPanel(ctx: *WindowContext) void {
|
||||
const fill_color = dvui.Color.black.opacity(0.25);
|
||||
var 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,
|
||||
},
|
||||
);
|
||||
{
|
||||
const active_doc = ctx.activeDocument();
|
||||
if (active_doc) |doc| {
|
||||
const content_rect_scale = panel.data().contentRectScale();
|
||||
canvas_view.canvasView(&doc.canvas, content_rect_scale);
|
||||
} else {
|
||||
noDocView(ctx);
|
||||
}
|
||||
}
|
||||
panel.deinit();
|
||||
}
|
||||
|
||||
fn noDocView(ctx: *WindowContext) void {
|
||||
var 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});
|
||||
};
|
||||
}
|
||||
}
|
||||
center.deinit();
|
||||
}
|
||||
27
src/ui/tab_bar.zig
Normal file
27
src/ui/tab_bar.zig
Normal file
@@ -0,0 +1,27 @@
|
||||
// Верхняя строка: вкладки документов + кнопка «Новый».
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
const WindowContext = @import("../WindowContext.zig");
|
||||
|
||||
pub fn tabBar(ctx: *WindowContext) void {
|
||||
var bar = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .horizontal },
|
||||
.{ .expand = .horizontal, .min_size_content = .{ .h = 32 }, .background = true, .padding = dvui.Rect.all(4) },
|
||||
);
|
||||
{
|
||||
for (ctx.documents.items, 0..) |_, i| {
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (dvui.button(@src(), "+", .{}, .{})) {
|
||||
ctx.addNewDocument() catch |err| {
|
||||
std.debug.print("addNewDocument error: {}\n", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
bar.deinit();
|
||||
}
|
||||
Reference in New Issue
Block a user