Compare commits
5 Commits
1dda9c9d15
...
1a94cc8bfd
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a94cc8bfd | |||
| aeda3ee0d0 | |||
| dd9d5deb92 | |||
| bd58286c98 | |||
| b896a67fd4 |
@@ -4,6 +4,7 @@ const dvui = @import("dvui");
|
|||||||
const Document = @import("models/Document.zig");
|
const Document = @import("models/Document.zig");
|
||||||
const RenderEngine = @import("render/RenderEngine.zig").RenderEngine;
|
const RenderEngine = @import("render/RenderEngine.zig").RenderEngine;
|
||||||
const ImageRect = @import("models/basic_models.zig").ImageRect;
|
const ImageRect = @import("models/basic_models.zig").ImageRect;
|
||||||
|
const Point2 = @import("models/basic_models.zig").Point2;
|
||||||
const Color = dvui.Color;
|
const Color = dvui.Color;
|
||||||
|
|
||||||
const Canvas = @This();
|
const Canvas = @This();
|
||||||
@@ -18,12 +19,12 @@ scroll: dvui.ScrollInfo = .{
|
|||||||
.horizontal = .auto,
|
.horizontal = .auto,
|
||||||
},
|
},
|
||||||
native_scaling: bool = true,
|
native_scaling: bool = true,
|
||||||
/// Максимальная частота перерисовки при зуме. 0 = без ограничения.
|
|
||||||
redraw_throttle_ms: u32 = 50,
|
redraw_throttle_ms: u32 = 50,
|
||||||
_visible_rect: ?ImageRect = null,
|
_visible_rect: ?ImageRect = null,
|
||||||
_zoom: f32 = 1,
|
_zoom: f32 = 1,
|
||||||
_redraw_pending: bool = false,
|
_redraw_pending: bool = false,
|
||||||
_last_redraw_time_ms: i64 = 0,
|
_last_redraw_time_ms: i64 = 0,
|
||||||
|
cursor_document_point: ?Point2 = null,
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine) Canvas {
|
pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine) Canvas {
|
||||||
return .{
|
return .{
|
||||||
@@ -40,9 +41,8 @@ pub fn deinit(self: *Canvas) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Заполнить canvas градиентом
|
|
||||||
pub fn redrawExample(self: *Canvas) !void {
|
pub fn redrawExample(self: *Canvas) !void {
|
||||||
const full = self.getScaledImageSize();
|
const full = self.getZoomedImageSize();
|
||||||
|
|
||||||
const vis: ImageRect = self._visible_rect orelse ImageRect{ .x = 0, .y = 0, .w = 0, .h = 0 };
|
const vis: ImageRect = self._visible_rect orelse ImageRect{ .x = 0, .y = 0, .w = 0, .h = 0 };
|
||||||
|
|
||||||
@@ -57,7 +57,6 @@ pub fn redrawExample(self: *Canvas) !void {
|
|||||||
const new_texture = self.render_engine.example(.{ .w = full.w, .h = full.h }, vis) catch null;
|
const new_texture = self.render_engine.example(.{ .w = full.w, .h = full.h }, vis) catch null;
|
||||||
|
|
||||||
if (new_texture) |tex| {
|
if (new_texture) |tex| {
|
||||||
// Удалить старую текстуру
|
|
||||||
if (self.texture) |old_tex| {
|
if (self.texture) |old_tex| {
|
||||||
dvui.Texture.destroyLater(old_tex);
|
dvui.Texture.destroyLater(old_tex);
|
||||||
}
|
}
|
||||||
@@ -67,7 +66,6 @@ pub fn redrawExample(self: *Canvas) !void {
|
|||||||
self._last_redraw_time_ms = std.time.milliTimestamp();
|
self._last_redraw_time_ms = std.time.milliTimestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ресетнуть example изображение в renderEngine
|
|
||||||
pub fn exampleReset(self: *Canvas) !void {
|
pub fn exampleReset(self: *Canvas) !void {
|
||||||
self.render_engine.exampleReset();
|
self.render_engine.exampleReset();
|
||||||
try self.redrawExample();
|
try self.redrawExample();
|
||||||
@@ -82,12 +80,10 @@ pub fn addZoom(self: *Canvas, value: f32) void {
|
|||||||
self._zoom = @max(self._zoom, 0.01);
|
self._zoom = @max(self._zoom, 0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Запросить перерисовку (выполнится с учётом redraw_throttle_ms при вызове processPendingRedraw).
|
|
||||||
pub fn requestRedraw(self: *Canvas) void {
|
pub fn requestRedraw(self: *Canvas) void {
|
||||||
self._redraw_pending = true;
|
self._redraw_pending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Выполнить отложенную перерисовку, если прошло не менее redraw_throttle_ms с прошлой отрисовки.
|
|
||||||
pub fn processPendingRedraw(self: *Canvas) !void {
|
pub fn processPendingRedraw(self: *Canvas) !void {
|
||||||
if (!self._redraw_pending) return;
|
if (!self._redraw_pending) return;
|
||||||
if (self.redraw_throttle_ms == 0) {
|
if (self.redraw_throttle_ms == 0) {
|
||||||
@@ -102,7 +98,7 @@ pub fn processPendingRedraw(self: *Canvas) !void {
|
|||||||
try self.redrawExample();
|
try self.redrawExample();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getScaledImageSize(self: Canvas) ImageRect {
|
pub fn getZoomedImageSize(self: Canvas) ImageRect {
|
||||||
const doc = self.document;
|
const doc = self.document;
|
||||||
return .{
|
return .{
|
||||||
.x = @intFromFloat(self.pos.x),
|
.x = @intFromFloat(self.pos.x),
|
||||||
@@ -112,11 +108,25 @@ pub fn getScaledImageSize(self: Canvas) ImageRect {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Обновить видимую часть изображения (в пикселях холста) и сохранить в `visible_rect`.
|
pub fn contentPointToDocument(self: Canvas, content_point: dvui.Point, natural_scale: f32) ?Point2 {
|
||||||
///
|
const img = self.getZoomedImageSize();
|
||||||
/// `viewport` и `scroll_offset` ожидаются в *physical* пикселях (т.е. уже умноженные на windowNaturalScale).
|
const left_n = @as(f32, @floatFromInt(img.x)) / natural_scale;
|
||||||
///
|
const top_n = @as(f32, @floatFromInt(img.y)) / natural_scale;
|
||||||
/// После обновления (или если текстуры ещё нет) перерисовывает текстуру, чтобы она содержала только видимую часть.
|
const right_n = @as(f32, @floatFromInt(img.x + img.w)) / natural_scale;
|
||||||
|
const bottom_n = @as(f32, @floatFromInt(img.y + img.h)) / natural_scale;
|
||||||
|
|
||||||
|
if (content_point.x < left_n or content_point.x >= right_n or
|
||||||
|
content_point.y < top_n or content_point.y >= bottom_n)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const px_x = content_point.x * natural_scale - @as(f32, @floatFromInt(img.x));
|
||||||
|
const px_y = content_point.y * natural_scale - @as(f32, @floatFromInt(img.y));
|
||||||
|
return Point2{
|
||||||
|
.x = px_x / self._zoom,
|
||||||
|
.y = px_y / self._zoom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) bool {
|
pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) bool {
|
||||||
const next = computeVisibleImageRect(self.*, viewport, scroll_offset);
|
const next = computeVisibleImageRect(self.*, viewport, scroll_offset);
|
||||||
var changed = false;
|
var changed = false;
|
||||||
@@ -134,16 +144,14 @@ pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn computeVisibleImageRect(self: Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) ImageRect {
|
fn computeVisibleImageRect(self: Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) ImageRect {
|
||||||
const image_rect = self.getScaledImageSize();
|
const image_rect = self.getZoomedImageSize();
|
||||||
|
|
||||||
const img_w: u32 = image_rect.w;
|
const img_w: u32 = image_rect.w;
|
||||||
const img_h: u32 = image_rect.h;
|
const img_h: u32 = image_rect.h;
|
||||||
|
|
||||||
// Видимый размер всегда равен размеру viewport, но не больше холста
|
|
||||||
const vis_w: u32 = @min(@as(u32, @intFromFloat(viewport.w)), img_w);
|
const vis_w: u32 = @min(@as(u32, @intFromFloat(viewport.w)), img_w);
|
||||||
const vis_h: u32 = @min(@as(u32, @intFromFloat(viewport.h)), img_h);
|
const vis_h: u32 = @min(@as(u32, @intFromFloat(viewport.h)), img_h);
|
||||||
|
|
||||||
// Вычисляем x и y на основе scroll_offset, clamped чтобы не выходить за границы
|
|
||||||
const raw_x: i64 = @intFromFloat(scroll_offset.x - @as(f32, @floatFromInt(image_rect.x)));
|
const raw_x: i64 = @intFromFloat(scroll_offset.x - @as(f32, @floatFromInt(image_rect.x)));
|
||||||
const raw_y: i64 = @intFromFloat(scroll_offset.y - @as(f32, @floatFromInt(image_rect.y)));
|
const raw_y: i64 = @intFromFloat(scroll_offset.y - @as(f32, @floatFromInt(image_rect.y)));
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ const basic_models = @import("models/basic_models.zig");
|
|||||||
|
|
||||||
const WindowContext = @This();
|
const WindowContext = @This();
|
||||||
|
|
||||||
/// Один открытый документ: документ + свой холст и движок рендера
|
|
||||||
pub const OpenDocument = struct {
|
pub const OpenDocument = struct {
|
||||||
document: Document,
|
document: Document,
|
||||||
cpu_render: CpuRenderEngine,
|
cpu_render: CpuRenderEngine,
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
|
|
||||||
/// Инициализировать по месту (canvas хранит указатель на cpu_render этого же экземпляра).
|
|
||||||
pub fn init(allocator: std.mem.Allocator, self: *OpenDocument) void {
|
pub fn init(allocator: std.mem.Allocator, self: *OpenDocument) void {
|
||||||
const default_size = basic_models.Size{ .width = 800, .height = 600 };
|
const default_size = basic_models.Size{ .width = 800, .height = 600 };
|
||||||
self.document = Document.init(allocator, default_size);
|
self.document = Document.init(allocator, default_size);
|
||||||
@@ -29,9 +27,7 @@ pub const OpenDocument = struct {
|
|||||||
|
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
frame_index: u64,
|
frame_index: u64,
|
||||||
/// Список открытых документов (вкладок): указатели на документ+холст
|
|
||||||
documents: std.ArrayList(*OpenDocument),
|
documents: std.ArrayList(*OpenDocument),
|
||||||
/// Индекс активной вкладки; null — ни один документ не выбран
|
|
||||||
active_document_index: ?usize,
|
active_document_index: ?usize,
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator) !WindowContext {
|
pub fn init(allocator: std.mem.Allocator) !WindowContext {
|
||||||
@@ -54,30 +50,27 @@ pub fn deinit(self: *WindowContext) void {
|
|||||||
self.documents.deinit(self.allocator);
|
self.documents.deinit(self.allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Вернуть указатель на активный открытый документ (null если нет выбранной вкладки).
|
|
||||||
pub fn activeDocument(self: *WindowContext) ?*OpenDocument {
|
pub fn activeDocument(self: *WindowContext) ?*OpenDocument {
|
||||||
const i = self.active_document_index orelse return null;
|
const i = self.active_document_index orelse return null;
|
||||||
if (i >= self.documents.items.len) return null;
|
if (i >= self.documents.items.len) return null;
|
||||||
return self.documents.items[i];
|
return self.documents.items[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Добавить новый документ и сделать его активным.
|
|
||||||
pub fn addNewDocument(self: *WindowContext) !void {
|
pub fn addNewDocument(self: *WindowContext) !void {
|
||||||
const ptr = try self.allocator.create(OpenDocument);
|
const ptr = try self.allocator.create(OpenDocument);
|
||||||
errdefer self.allocator.destroy(ptr);
|
errdefer self.allocator.destroy(ptr);
|
||||||
OpenDocument.init(self.allocator, ptr);
|
OpenDocument.init(self.allocator, ptr);
|
||||||
|
try ptr.document.addRandomShapes(std.crypto.random);
|
||||||
try self.documents.append(self.allocator, ptr);
|
try self.documents.append(self.allocator, ptr);
|
||||||
self.active_document_index = self.documents.items.len - 1;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Закрыть вкладку по индексу; активная вкладка сдвигается при необходимости.
|
|
||||||
pub fn closeDocument(self: *WindowContext, index: usize) void {
|
pub fn closeDocument(self: *WindowContext, index: usize) void {
|
||||||
if (index >= self.documents.items.len) return;
|
if (index >= self.documents.items.len) return;
|
||||||
const open_doc = self.documents.items[index];
|
const open_doc = self.documents.items[index];
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const basic_models = @import("basic_models.zig");
|
const basic_models = @import("basic_models.zig");
|
||||||
const properties = @import("Property.zig");
|
|
||||||
const Property = properties.Property;
|
|
||||||
const Size = basic_models.Size;
|
const Size = basic_models.Size;
|
||||||
const Document = @This();
|
const Document = @This();
|
||||||
|
|
||||||
|
pub const Object = @import("Object.zig");
|
||||||
|
|
||||||
size: Size,
|
size: Size,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
/// Корневые объекты документа (вложенность через Object.children).
|
|
||||||
objects: std.ArrayList(Object),
|
objects: std.ArrayList(Object),
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, size: Size) Document {
|
pub fn init(allocator: std.mem.Allocator, size: Size) Document {
|
||||||
@@ -23,112 +22,61 @@ pub fn deinit(self: *Document) void {
|
|||||||
self.objects.deinit(self.allocator);
|
self.objects.deinit(self.allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Добавить корневой объект в документ (клонирует в allocator документа).
|
|
||||||
pub fn addObject(self: *Document, template: Object) !void {
|
pub fn addObject(self: *Document, template: Object) !void {
|
||||||
const obj = try template.clone(self.allocator);
|
const obj = try template.clone(self.allocator);
|
||||||
try self.objects.append(self.allocator, obj);
|
try self.objects.append(self.allocator, obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Тип фигуры: определяет, как RenderEngine интерпретирует свойства и рисует объект.
|
pub fn addShape(self: *Document, parent: ?*Object, shape: Object.ShapeKind) !void {
|
||||||
pub const ShapeKind = enum {
|
const obj = switch (shape) {
|
||||||
rect,
|
.line => try Object.createLine(self.allocator),
|
||||||
ellipse,
|
.ellipse => try Object.createEllipse(self.allocator),
|
||||||
line,
|
.broken => try Object.createBrokenLine(self.allocator),
|
||||||
path,
|
.arc => return error.ArcNotImplemented,
|
||||||
};
|
};
|
||||||
|
if (parent) |p| {
|
||||||
/// Объект документа: тип фигуры, свойства и вложенные дочерние объекты.
|
try p.addChild(self.allocator, obj);
|
||||||
/// Типы объектов задаются конструкторами; UI и RenderEngine работают с union Property.
|
} else {
|
||||||
pub const Object = struct {
|
try self.addObject(obj);
|
||||||
shape: ShapeKind,
|
|
||||||
properties: std.ArrayList(Property),
|
|
||||||
/// Вложенные объекты (дерево).
|
|
||||||
children: std.ArrayList(Object),
|
|
||||||
|
|
||||||
/// Найти свойство по тегу и вернуть вариант union (caller делает switch).
|
|
||||||
pub fn getProperty(self: Object, tag: std.meta.Tag(Property)) ?Property {
|
|
||||||
for (self.properties.items) |prop| {
|
|
||||||
if (std.meta.activeTag(prop) == tag) return prop;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Установить свойство: если уже есть с таким тегом — заменить, иначе добавить.
|
|
||||||
pub fn setProperty(self: *Object, allocator: std.mem.Allocator, prop: Property) !void {
|
|
||||||
for (self.properties.items, 0..) |*p, i| {
|
|
||||||
if (std.meta.activeTag(p.*) == std.meta.activeTag(prop)) {
|
|
||||||
self.properties.items[i] = prop;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try self.properties.append(allocator, prop);
|
|
||||||
|
fn randomShapeKind(rng: std.Random) Object.ShapeKind {
|
||||||
|
const shapes_implemented = [_]Object.ShapeKind{ .line, .ellipse, .broken };
|
||||||
|
return shapes_implemented[rng.intRangeLessThan(usize, 0, shapes_implemented.len)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Добавить дочерний объект (клонирует в переданный allocator).
|
/// Создаёт случайное количество фигур в документе (в т.ч. вложенных).
|
||||||
pub fn addChild(self: *Object, allocator: std.mem.Allocator, template: Object) !void {
|
/// Используются только реализованные типы: line, ellipse, broken.
|
||||||
const obj = try template.clone(allocator);
|
/// Ограничение max_total предотвращает экспоненциальный рост и переполнение.
|
||||||
try self.children.append(allocator, obj);
|
pub fn addRandomShapes(self: *Document, rng: std.Random) !void {
|
||||||
|
const max_total: usize = 80;
|
||||||
|
var total_count: usize = 0;
|
||||||
|
|
||||||
|
const n_root = rng.intRangeLessThan(usize, 1, 5);
|
||||||
|
for (0..n_root) |_| {
|
||||||
|
if (total_count >= max_total) break;
|
||||||
|
try self.addShape(null, randomShapeKind(rng));
|
||||||
|
total_count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Клонировать объект рекурсивно (свойства и дети).
|
var stack = std.ArrayList(*Object).empty;
|
||||||
pub fn clone(self: Object, allocator: std.mem.Allocator) !Object {
|
defer stack.deinit(self.allocator);
|
||||||
var properties_list = std.ArrayList(Property).empty;
|
for (self.objects.items) |*obj| {
|
||||||
errdefer properties_list.deinit(allocator);
|
try stack.append(self.allocator, obj);
|
||||||
try properties_list.appendSlice(allocator, self.properties.items);
|
|
||||||
|
|
||||||
var children_list = std.ArrayList(Object).empty;
|
|
||||||
errdefer children_list.deinit(allocator);
|
|
||||||
for (self.children.items) |child| {
|
|
||||||
try children_list.append(allocator, try child.clone(allocator));
|
|
||||||
}
|
}
|
||||||
|
while (stack.pop()) |obj| {
|
||||||
return .{
|
if (total_count >= max_total) continue;
|
||||||
.shape = self.shape,
|
const n_children = rng.intRangeLessThan(usize, 0, 2);
|
||||||
.properties = properties_list,
|
const base_len = obj.children.items.len;
|
||||||
.children = children_list,
|
for (0..n_children) |_| {
|
||||||
};
|
if (total_count >= max_total) break;
|
||||||
|
try self.addShape(obj, randomShapeKind(rng));
|
||||||
|
total_count += 1;
|
||||||
}
|
}
|
||||||
|
// Пушим в стек только после всех append, чтобы не держать указатели при реаллокации obj.children
|
||||||
pub fn deinit(self: *Object, allocator: std.mem.Allocator) void {
|
for (obj.children.items[base_len..]) |*child| {
|
||||||
for (self.children.items) |*child| child.deinit(allocator);
|
try stack.append(self.allocator, child);
|
||||||
self.children.deinit(allocator);
|
|
||||||
self.properties.deinit(allocator);
|
|
||||||
self.* = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Базовый объект с общим набором свойств (для внутреннего использования конструкторами).
|
|
||||||
fn createWithCommon(allocator: std.mem.Allocator, shape: ShapeKind) !Object {
|
|
||||||
const common = properties.defaultCommonProperties();
|
|
||||||
var properties_list = std.ArrayList(Property).empty;
|
|
||||||
errdefer properties_list.deinit(allocator);
|
|
||||||
try properties_list.appendSlice(allocator, &common);
|
|
||||||
return .{
|
|
||||||
.shape = shape,
|
|
||||||
.properties = properties_list,
|
|
||||||
.children = std.ArrayList(Object).empty,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Публичные конструкторы: базовый объект + одно свойство фигуры ---
|
|
||||||
|
|
||||||
pub fn createRect(allocator: std.mem.Allocator) !Object {
|
|
||||||
var obj = try createWithCommon(allocator, .rect);
|
|
||||||
errdefer obj.deinit(allocator);
|
|
||||||
try obj.properties.append(allocator, .{ .size = .{ .width = 100, .height = 100 } });
|
|
||||||
return obj;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createEllipse(allocator: std.mem.Allocator) !Object {
|
|
||||||
var obj = try createWithCommon(allocator, .ellipse);
|
|
||||||
errdefer obj.deinit(allocator);
|
|
||||||
try obj.properties.append(allocator, .{ .radii = .{ .x = 50, .y = 50 } });
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn createLine(allocator: std.mem.Allocator) !Object {
|
|
||||||
var obj = try createWithCommon(allocator, .line);
|
|
||||||
errdefer obj.deinit(allocator);
|
|
||||||
try obj.properties.append(allocator, .{ .end_point = .{ .x = 100, .y = 0 } });
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
108
src/models/Object.zig
Normal file
108
src/models/Object.zig
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const basic_models = @import("basic_models.zig");
|
||||||
|
const Size = basic_models.Size;
|
||||||
|
const Point2 = basic_models.Point2;
|
||||||
|
const Property = @import("Property.zig").Property;
|
||||||
|
const PropertyData = @import("Property.zig").Data;
|
||||||
|
const defaultCommonProperties = @import("Property.zig").defaultCommonProperties;
|
||||||
|
const Object = @This();
|
||||||
|
|
||||||
|
pub const ShapeKind = enum {
|
||||||
|
line,
|
||||||
|
ellipse,
|
||||||
|
arc,
|
||||||
|
broken,
|
||||||
|
};
|
||||||
|
|
||||||
|
shape: ShapeKind,
|
||||||
|
properties: std.ArrayList(Property),
|
||||||
|
children: std.ArrayList(Object),
|
||||||
|
|
||||||
|
pub fn getProperty(self: Object, tag: std.meta.Tag(PropertyData)) ?*const PropertyData {
|
||||||
|
for (self.properties.items) |*prop| {
|
||||||
|
if (std.meta.activeTag(prop.data) == tag) return &prop.data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setProperty(self: *Object, allocator: std.mem.Allocator, prop: Property) !void {
|
||||||
|
for (self.properties.items, 0..) |*p, i| {
|
||||||
|
if (std.meta.activeTag(p.data) == std.meta.activeTag(prop.data)) {
|
||||||
|
if (p.data == .points) p.data.points.deinit(allocator);
|
||||||
|
self.properties.items[i] = prop;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try self.properties.append(allocator, prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addChild(self: *Object, allocator: std.mem.Allocator, template: Object) !void {
|
||||||
|
const obj = try template.clone(allocator);
|
||||||
|
try self.children.append(allocator, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clone(self: Object, allocator: std.mem.Allocator) !Object {
|
||||||
|
var properties_list = std.ArrayList(Property).empty;
|
||||||
|
errdefer properties_list.deinit(allocator);
|
||||||
|
for (self.properties.items) |prop| {
|
||||||
|
try properties_list.append(allocator, try prop.clone(allocator));
|
||||||
|
}
|
||||||
|
|
||||||
|
var children_list = std.ArrayList(Object).empty;
|
||||||
|
errdefer children_list.deinit(allocator);
|
||||||
|
for (self.children.items) |child| {
|
||||||
|
try children_list.append(allocator, try child.clone(allocator));
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.shape = self.shape,
|
||||||
|
.properties = properties_list,
|
||||||
|
.children = children_list,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Object, allocator: std.mem.Allocator) void {
|
||||||
|
for (self.children.items) |*child| child.deinit(allocator);
|
||||||
|
self.children.deinit(allocator);
|
||||||
|
for (self.properties.items) |*prop| prop.deinit(allocator);
|
||||||
|
self.properties.deinit(allocator);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn createWithCommonProperties(allocator: std.mem.Allocator, shape: ShapeKind) !Object {
|
||||||
|
var properties_list = std.ArrayList(Property).empty;
|
||||||
|
errdefer properties_list.deinit(allocator);
|
||||||
|
for (defaultCommonProperties) |prop| try properties_list.append(allocator, prop);
|
||||||
|
return .{
|
||||||
|
.shape = shape,
|
||||||
|
.properties = properties_list,
|
||||||
|
.children = std.ArrayList(Object).empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createEllipse(allocator: std.mem.Allocator) !Object {
|
||||||
|
var obj = try createWithCommonProperties(allocator, .ellipse);
|
||||||
|
errdefer obj.deinit(allocator);
|
||||||
|
try obj.properties.append(allocator, .{ .data = .{ .radii = .{ .x = 50, .y = 50 } } });
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createLine(allocator: std.mem.Allocator) !Object {
|
||||||
|
var obj = try createWithCommonProperties(allocator, .line);
|
||||||
|
errdefer obj.deinit(allocator);
|
||||||
|
try obj.properties.append(allocator, .{ .data = .{ .end_point = .{ .x = 100, .y = 0 } } });
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createBrokenLine(allocator: std.mem.Allocator) !Object {
|
||||||
|
var obj = try createWithCommonProperties(allocator, .broken);
|
||||||
|
errdefer obj.deinit(allocator);
|
||||||
|
var points = std.ArrayList(Point2).empty;
|
||||||
|
try points.appendSlice(allocator, &.{
|
||||||
|
.{ .x = 0, .y = 0 },
|
||||||
|
.{ .x = 80, .y = 0 },
|
||||||
|
.{ .x = 80, .y = 60 },
|
||||||
|
});
|
||||||
|
try obj.properties.append(allocator, .{ .data = .{ .points = points } });
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
@@ -1,62 +1,77 @@
|
|||||||
// Модель свойств объекта документа.
|
const std = @import("std");
|
||||||
// Каждое свойство — отдельный тип в union; UI и RenderEngine работают с полиморфным Property.
|
|
||||||
// Комплексные значения (размер, радиусы) — один вариант свойства, а не несколько полей.
|
|
||||||
|
|
||||||
const basic_models = @import("basic_models.zig");
|
const basic_models = @import("basic_models.zig");
|
||||||
const Point2 = basic_models.Point2;
|
const Point2 = basic_models.Point2;
|
||||||
const Scale2 = basic_models.Scale2;
|
const Scale2 = basic_models.Scale2;
|
||||||
const Size = basic_models.Size;
|
const Size = basic_models.Size;
|
||||||
const Radii = basic_models.Radii;
|
const Radii = basic_models.Radii;
|
||||||
|
|
||||||
/// Одно свойство объекта: полиморфный union.
|
pub const Data = union(enum) {
|
||||||
/// Варианты — целостные значения (size, radii), не разбитые на отдельные поля.
|
|
||||||
/// UI перебирает свойства и по тегу показывает нужный редактор;
|
|
||||||
/// RenderEngine по тегу фигуры читает нужные свойства для отрисовки.
|
|
||||||
pub const Property = union(enum) {
|
|
||||||
// --- Общие для всех фигур (см. defaultCommonProperties) ---
|
|
||||||
/// Левый верхний угол bbox для rect/path; центр для ellipse; начало для line.
|
|
||||||
position: Point2,
|
position: Point2,
|
||||||
|
angle: f32,
|
||||||
scale: Scale2,
|
scale: Scale2,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
opacity: f32,
|
opacity: f32,
|
||||||
locked: bool,
|
locked: bool,
|
||||||
|
|
||||||
// --- Прямоугольник: один вариант ---
|
|
||||||
size: Size,
|
size: Size,
|
||||||
|
|
||||||
// --- Эллипс: один вариант ---
|
|
||||||
radii: Radii,
|
radii: Radii,
|
||||||
|
|
||||||
// --- Линия: конечная точка (относительно position) ---
|
|
||||||
end_point: Point2,
|
end_point: Point2,
|
||||||
|
|
||||||
// --- Визуал (опционально для будущего) ---
|
points: std.ArrayList(Point2),
|
||||||
|
|
||||||
fill_rgba: u32,
|
fill_rgba: u32,
|
||||||
stroke_rgba: u32,
|
stroke_rgba: u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
const std = @import("std");
|
pub const Property = struct {
|
||||||
|
data: Data,
|
||||||
|
|
||||||
/// Общий набор свойств по умолчанию для любого объекта (одно место определения).
|
pub fn deinit(self: *Property, allocator: std.mem.Allocator) void {
|
||||||
/// Конструкторы фигур добавляют сначала его, затем специфичные свойства.
|
switch (self.data) {
|
||||||
pub fn defaultCommonProperties() []Property {
|
.points => |*list| list.deinit(allocator),
|
||||||
return .{
|
else => {},
|
||||||
|
}
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clone(self: Property, allocator: std.mem.Allocator) !Property {
|
||||||
|
return switch (self.data) {
|
||||||
|
.points => |list| .{
|
||||||
|
.data = .{
|
||||||
|
.points = try list.clone(allocator),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
else => .{ .data = self.data },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const default_common_data = [_]Data{
|
||||||
.{ .position = .{ .x = 0, .y = 0 } },
|
.{ .position = .{ .x = 0, .y = 0 } },
|
||||||
|
.{ .angle = 0 },
|
||||||
.{ .scale = .{ .scale_x = 1, .scale_y = 1 } },
|
.{ .scale = .{ .scale_x = 1, .scale_y = 1 } },
|
||||||
.{ .visible = true },
|
.{ .visible = true },
|
||||||
.{ .opacity = 1.0 },
|
.{ .opacity = 1.0 },
|
||||||
.{ .locked = false },
|
.{ .locked = false },
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
test "Property is union" {
|
pub const defaultCommonProperties: [default_common_data.len]Property = blk: {
|
||||||
const p: Property = .{ .opacity = 0.5 };
|
var result: [default_common_data.len]Property = undefined;
|
||||||
try std.testing.expect(p == .opacity);
|
for (default_common_data, &result) |d, *p| {
|
||||||
try std.testing.expect(p.opacity == 0.5);
|
p.* = .{ .data = d };
|
||||||
|
}
|
||||||
|
break :blk result;
|
||||||
|
};
|
||||||
|
|
||||||
|
test "Property wrapper and Data" {
|
||||||
|
const p = Property{ .data = .{ .opacity = 0.5 } };
|
||||||
|
try std.testing.expect(p.data == .opacity);
|
||||||
|
try std.testing.expect(p.data.opacity == 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "common properties" {
|
test "common properties" {
|
||||||
const common = defaultCommonProperties();
|
try std.testing.expect(defaultCommonProperties[0].data == .position);
|
||||||
try std.testing.expect(common[0].position.x == 0);
|
try std.testing.expect(defaultCommonProperties[0].data.position.x == 0);
|
||||||
try std.testing.expect(common[2].visible == true);
|
try std.testing.expect(defaultCommonProperties[3].data == .visible);
|
||||||
|
try std.testing.expect(defaultCommonProperties[3].data.visible == true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,19 +15,16 @@ pub const Size = struct {
|
|||||||
height: f32,
|
height: f32,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Точка в 2D (документные единицы)
|
|
||||||
pub const Point2 = struct {
|
pub const Point2 = struct {
|
||||||
x: f32 = 0,
|
x: f32 = 0,
|
||||||
y: f32 = 0,
|
y: f32 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Радиусы эллипса по осям (одно свойство).
|
|
||||||
pub const Radii = struct {
|
pub const Radii = struct {
|
||||||
x: f32,
|
x: f32,
|
||||||
y: f32,
|
y: f32,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Масштаб объекта
|
|
||||||
pub const Scale2 = struct {
|
pub const Scale2 = struct {
|
||||||
scale_x: f32 = 1,
|
scale_x: f32 = 1,
|
||||||
scale_y: f32 = 1,
|
scale_y: f32 = 1,
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ pub fn init(allocator: Allocator, render_type: Type) CpuRenderEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn exampleReset(self: *CpuRenderEngine) void {
|
pub fn exampleReset(self: *CpuRenderEngine) void {
|
||||||
// Сгенерировать случайные цвета градиента
|
|
||||||
var prng = std.Random.DefaultPrng.init(@intCast(std.time.microTimestamp()));
|
var prng = std.Random.DefaultPrng.init(@intCast(std.time.microTimestamp()));
|
||||||
const random = prng.random();
|
const random = prng.random();
|
||||||
self.gradient_start = Color.PMA{ .r = random.int(u8), .g = random.int(u8), .b = random.int(u8), .a = 255 };
|
self.gradient_start = Color.PMA{ .r = random.int(u8), .g = random.int(u8), .b = random.int(u8), .a = 255 };
|
||||||
@@ -64,12 +63,12 @@ fn renderSquares(self: CpuRenderEngine, pixels: []Color.PMA, canvas_size: ImageS
|
|||||||
_ = self;
|
_ = self;
|
||||||
|
|
||||||
const colors = [_]Color.PMA{
|
const colors = [_]Color.PMA{
|
||||||
.{ .r = 255, .g = 0, .b = 0, .a = 255 }, // red
|
.{ .r = 255, .g = 0, .b = 0, .a = 255 },
|
||||||
.{ .r = 255, .g = 165, .b = 0, .a = 255 }, // orange
|
.{ .r = 255, .g = 165, .b = 0, .a = 255 },
|
||||||
.{ .r = 255, .g = 255, .b = 0, .a = 255 }, // yellow
|
.{ .r = 255, .g = 255, .b = 0, .a = 255 },
|
||||||
.{ .r = 0, .g = 255, .b = 0, .a = 255 }, // green
|
.{ .r = 0, .g = 255, .b = 0, .a = 255 },
|
||||||
.{ .r = 0, .g = 255, .b = 255, .a = 255 }, // cyan
|
.{ .r = 0, .g = 255, .b = 255, .a = 255 },
|
||||||
.{ .r = 0, .g = 0, .b = 255, .a = 255 }, // blue
|
.{ .r = 0, .g = 0, .b = 255, .a = 255 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const squares_num = 5;
|
const squares_num = 5;
|
||||||
@@ -106,7 +105,6 @@ fn renderSquares(self: CpuRenderEngine, pixels: []Color.PMA, canvas_size: ImageS
|
|||||||
const canvas_x = x + visible_rect.x;
|
const canvas_x = x + visible_rect.x;
|
||||||
if (canvas_x >= canvas_size.w) continue;
|
if (canvas_x >= canvas_size.w) continue;
|
||||||
|
|
||||||
// Check vertical line index
|
|
||||||
var vertical_index: ?u32 = null;
|
var vertical_index: ?u32 = null;
|
||||||
for (0..x_pos.len) |i| {
|
for (0..x_pos.len) |i| {
|
||||||
if (canvas_x >= x_pos[i] and canvas_x < x_pos[i] + thikness) {
|
if (canvas_x >= x_pos[i] and canvas_x < x_pos[i] + thikness) {
|
||||||
@@ -115,7 +113,6 @@ fn renderSquares(self: CpuRenderEngine, pixels: []Color.PMA, canvas_size: ImageS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check horizontal line index
|
|
||||||
var horizontal_index: ?u32 = null;
|
var horizontal_index: ?u32 = null;
|
||||||
for (0..y_pos.len) |i| {
|
for (0..y_pos.len) |i| {
|
||||||
if (canvas_y >= y_pos[i] and canvas_y < y_pos[i] + thikness) {
|
if (canvas_y >= y_pos[i] and canvas_y < y_pos[i] + thikness) {
|
||||||
@@ -129,7 +126,6 @@ fn renderSquares(self: CpuRenderEngine, pixels: []Color.PMA, canvas_size: ImageS
|
|||||||
} else if (horizontal_index) |idx| {
|
} else if (horizontal_index) |idx| {
|
||||||
pixels[y * visible_rect.w + x] = colors[idx];
|
pixels[y * visible_rect.w + x] = colors[idx];
|
||||||
} else {
|
} else {
|
||||||
// Find square
|
|
||||||
var square_x: u32 = 0;
|
var square_x: u32 = 0;
|
||||||
for (0..squares_num) |i| {
|
for (0..squares_num) |i| {
|
||||||
if (canvas_x >= x_pos[i] + thikness and canvas_x < x_pos[i + 1]) {
|
if (canvas_x >= x_pos[i] + thikness and canvas_x < x_pos[i + 1]) {
|
||||||
@@ -161,12 +157,9 @@ pub fn example(self: CpuRenderEngine, canvas_size: ImageSize, visible_rect: Imag
|
|||||||
const width = visible_rect.w;
|
const width = visible_rect.w;
|
||||||
const height = visible_rect.h;
|
const height = visible_rect.h;
|
||||||
|
|
||||||
// Выделить буфер пиксельных данных
|
|
||||||
const pixels = try self._allocator.alloc(Color.PMA, @as(usize, width) * height);
|
const pixels = try self._allocator.alloc(Color.PMA, @as(usize, width) * height);
|
||||||
defer self._allocator.free(pixels);
|
defer self._allocator.free(pixels);
|
||||||
|
|
||||||
// std.debug.print("w={any}, fw={any};\th={any}, fh={any}\n", .{ width, full_w, height, full_h });
|
|
||||||
|
|
||||||
switch (self.type) {
|
switch (self.type) {
|
||||||
.Gradient => self.renderGradient(pixels, width, height, full_w, full_h, visible_rect),
|
.Gradient => self.renderGradient(pixels, width, height, full_w, full_h, visible_rect),
|
||||||
.Squares => self.renderSquares(pixels, canvas_size, visible_rect),
|
.Squares => self.renderSquares(pixels, canvas_size, visible_rect),
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Интерфейс для рендеринга документа
|
|
||||||
const dvui = @import("dvui");
|
const dvui = @import("dvui");
|
||||||
const CpuRenderEngine = @import("CpuRenderEngine.zig");
|
const CpuRenderEngine = @import("CpuRenderEngine.zig");
|
||||||
const basic_models = @import("../models/basic_models.zig");
|
const basic_models = @import("../models/basic_models.zig");
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
// Test root for `zig build test`.
|
// Корень для `zig build test`. Тесты из импортированных здесь модулей выполняются (в Zig не подтягиваются из транзитивных импортов).
|
||||||
// Import modules here to ensure their `test` blocks are discovered.
|
// Добавляй сюда _ = @import("path/to/module.zig"); для каждого модуля с test-блоками.
|
||||||
|
// Чтобы увидеть список всех тестов: после `zig build test` выполни `./zig-out/bin/test`.
|
||||||
test "module test discovery" {
|
test "discover tests" {
|
||||||
_ = @import("render/CpuRenderEngine.zig");
|
_ = @import("main.zig");
|
||||||
|
_ = @import("models/Property.zig");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Убедиться, что выполнились все ожидаемые тесты: этот тест пройдёт только если до него дошли (т.е. все предыдущие прошли).
|
||||||
|
test "all module tests completed" {
|
||||||
|
const std = @import("std");
|
||||||
|
std.debug.print("\n (все тесты модулей выполнены)\n", .{});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Виджет холста: скролл, текстура, зум по Ctrl+колёсико.
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const dvui = @import("dvui");
|
const dvui = @import("dvui");
|
||||||
const dvui_ext = @import("dvui_ext.zig");
|
const dvui_ext = @import("dvui_ext.zig");
|
||||||
@@ -22,6 +21,7 @@ pub fn canvasView(canvas: *Canvas, content_rect_scale: dvui.RectScale) void {
|
|||||||
{
|
{
|
||||||
drawCanvasContent(canvas, scroll);
|
drawCanvasContent(canvas, scroll);
|
||||||
handleCanvasZoom(canvas, scroll);
|
handleCanvasZoom(canvas, scroll);
|
||||||
|
handleCanvasMouse(canvas, scroll);
|
||||||
}
|
}
|
||||||
scroll.deinit();
|
scroll.deinit();
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ pub fn canvasView(canvas: *Canvas, content_rect_scale: dvui.RectScale) void {
|
|||||||
|
|
||||||
fn drawCanvasContent(canvas: *Canvas, scroll: anytype) void {
|
fn drawCanvasContent(canvas: *Canvas, scroll: anytype) void {
|
||||||
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
|
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
|
||||||
const img_size = canvas.getScaledImageSize();
|
const img_size = canvas.getZoomedImageSize();
|
||||||
const viewport_rect = scroll.data().contentRect();
|
const viewport_rect = scroll.data().contentRect();
|
||||||
const scroll_current = dvui.Point{ .x = canvas.scroll.viewport.x, .y = canvas.scroll.viewport.y };
|
const scroll_current = dvui.Point{ .x = canvas.scroll.viewport.x, .y = canvas.scroll.viewport.y };
|
||||||
|
|
||||||
@@ -118,3 +118,26 @@ fn handleCanvasZoom(canvas: *Canvas, scroll: anytype) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handleCanvasMouse(canvas: *Canvas, scroll: anytype) void {
|
||||||
|
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
|
||||||
|
|
||||||
|
for (dvui.events()) |*e| {
|
||||||
|
switch (e.evt) {
|
||||||
|
.mouse => |*mouse| {
|
||||||
|
if (mouse.action != .press or mouse.button != .left) continue;
|
||||||
|
if (!dvui.eventMatchSimple(e, scroll.data())) 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,
|
||||||
|
};
|
||||||
|
canvas.cursor_document_point = canvas.contentPointToDocument(content_pt, natural_scale);
|
||||||
|
if (canvas.cursor_document_point) |point|
|
||||||
|
std.debug.print("cursor_document_point: {}\n", .{point});
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Расширения для dvui
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const dvui = @import("dvui");
|
const dvui = @import("dvui");
|
||||||
const TexturedBox = @import("./types/TexturedBox.zig");
|
const TexturedBox = @import("./types/TexturedBox.zig");
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
// Корневой кадр UI: разметка и сборка панелей.
|
|
||||||
const dvui = @import("dvui");
|
const dvui = @import("dvui");
|
||||||
const WindowContext = @import("../WindowContext.zig");
|
const WindowContext = @import("../WindowContext.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");
|
||||||
|
|
||||||
/// Отрисовать один кадр GUI. Возвращает false при закрытии окна/выходе.
|
|
||||||
pub fn guiFrame(ctx: *WindowContext) bool {
|
pub fn guiFrame(ctx: *WindowContext) bool {
|
||||||
for (dvui.events()) |*e| {
|
for (dvui.events()) |*e| {
|
||||||
if (e.evt == .window and e.evt.window.action == .close) return false;
|
if (e.evt == .window and e.evt.window.action == .close) return false;
|
||||||
@@ -24,16 +22,8 @@ pub fn guiFrame(ctx: *WindowContext) bool {
|
|||||||
{
|
{
|
||||||
left_panel.leftPanel(ctx);
|
left_panel.leftPanel(ctx);
|
||||||
|
|
||||||
var back = dvui.box(
|
|
||||||
@src(),
|
|
||||||
.{ .dir = .horizontal },
|
|
||||||
.{ .expand = .both, .padding = dvui.Rect.all(12), .background = true },
|
|
||||||
);
|
|
||||||
{
|
|
||||||
right_panel.rightPanel(ctx);
|
right_panel.rightPanel(ctx);
|
||||||
}
|
}
|
||||||
back.deinit();
|
|
||||||
}
|
|
||||||
content_row.deinit();
|
content_row.deinit();
|
||||||
}
|
}
|
||||||
root.deinit();
|
root.deinit();
|
||||||
|
|||||||
@@ -1,15 +1,112 @@
|
|||||||
// Левая панель: инструменты для активного документа (scaling, тип рендера).
|
|
||||||
const dvui = @import("dvui");
|
const dvui = @import("dvui");
|
||||||
const WindowContext = @import("../WindowContext.zig");
|
const WindowContext = @import("../WindowContext.zig");
|
||||||
|
const Document = @import("../models/Document.zig");
|
||||||
|
const Object = Document.Object;
|
||||||
|
|
||||||
|
const panel_gap: f32 = 12;
|
||||||
|
const panel_padding: f32 = 5;
|
||||||
|
const panel_radius: f32 = 24;
|
||||||
|
const fill_color = dvui.Color.black.opacity(0.2);
|
||||||
|
|
||||||
|
fn shapeLabel(shape: Object.ShapeKind) []const u8 {
|
||||||
|
return switch (shape) {
|
||||||
|
.line => "Line",
|
||||||
|
.ellipse => "Ellipse",
|
||||||
|
.arc => "Arc",
|
||||||
|
.broken => "Broken line",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn objectTreeRow(obj: *const Object, depth: u32, row_id: *usize) void {
|
||||||
|
const id = row_id.*;
|
||||||
|
row_id.* += 1;
|
||||||
|
const indent_px = depth * 18;
|
||||||
|
var row = dvui.box(
|
||||||
|
@src(),
|
||||||
|
.{ .dir = .horizontal },
|
||||||
|
.{ .padding = dvui.Rect{ .x = @floatFromInt(indent_px) }, .id_extra = id },
|
||||||
|
);
|
||||||
|
{
|
||||||
|
dvui.labelNoFmt(@src(), shapeLabel(obj.shape), .{}, .{ .id_extra = id });
|
||||||
|
}
|
||||||
|
row.deinit();
|
||||||
|
for (obj.children.items) |*child| {
|
||||||
|
objectTreeRow(child, depth + 1, row_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn objectTree(ctx: *WindowContext) void {
|
||||||
|
const active_doc = ctx.activeDocument();
|
||||||
|
if (active_doc) |open_doc| {
|
||||||
|
const doc = &open_doc.document;
|
||||||
|
if (doc.objects.items.len == 0) {
|
||||||
|
dvui.label(@src(), "No objects", .{}, .{});
|
||||||
|
} else {
|
||||||
|
var row_id: usize = 0;
|
||||||
|
for (doc.objects.items) |*obj| {
|
||||||
|
objectTreeRow(obj, 0, &row_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dvui.label(@src(), "No document", .{}, .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn leftPanel(ctx: *WindowContext) void {
|
pub fn leftPanel(ctx: *WindowContext) void {
|
||||||
|
var padding = dvui.Rect.all(panel_gap);
|
||||||
|
padding.w = 0;
|
||||||
var panel = dvui.box(
|
var panel = dvui.box(
|
||||||
@src(),
|
@src(),
|
||||||
.{ .dir = .vertical },
|
.{ .dir = .vertical },
|
||||||
.{ .expand = .vertical, .min_size_content = .{ .w = 200 }, .background = true },
|
.{
|
||||||
|
.expand = .vertical,
|
||||||
|
.min_size_content = .{ .w = 220 },
|
||||||
|
.background = true,
|
||||||
|
.padding = padding,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
{
|
{
|
||||||
dvui.label(@src(), "Tools", .{}, .{});
|
// Верхняя часть: дерево объектов
|
||||||
|
var tree_section = dvui.box(
|
||||||
|
@src(),
|
||||||
|
.{ .dir = .vertical },
|
||||||
|
.{
|
||||||
|
.expand = .both,
|
||||||
|
.padding = dvui.Rect.all(panel_padding),
|
||||||
|
.corner_radius = dvui.Rect.all(panel_radius),
|
||||||
|
.color_fill = fill_color,
|
||||||
|
.background = true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
{
|
||||||
|
dvui.label(@src(), "Objects", .{}, .{});
|
||||||
|
var scroll = dvui.scrollArea(
|
||||||
|
@src(),
|
||||||
|
.{ .vertical = .auto },
|
||||||
|
.{ .expand = .vertical, .background = false },
|
||||||
|
);
|
||||||
|
{
|
||||||
|
objectTree(ctx);
|
||||||
|
}
|
||||||
|
scroll.deinit();
|
||||||
|
}
|
||||||
|
tree_section.deinit();
|
||||||
|
|
||||||
|
// Нижняя часть: настройки
|
||||||
|
var settings_section = dvui.box(
|
||||||
|
@src(),
|
||||||
|
.{ .dir = .vertical },
|
||||||
|
.{
|
||||||
|
.expand = .horizontal,
|
||||||
|
.margin = .{ .y = 5 },
|
||||||
|
.padding = dvui.Rect.all(panel_padding),
|
||||||
|
.corner_radius = dvui.Rect.all(panel_radius),
|
||||||
|
.color_fill = fill_color,
|
||||||
|
.background = true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
{
|
||||||
|
dvui.label(@src(), "Settings", .{}, .{});
|
||||||
|
|
||||||
const active_doc = ctx.activeDocument();
|
const active_doc = ctx.activeDocument();
|
||||||
if (active_doc) |doc| {
|
if (active_doc) |doc| {
|
||||||
@@ -27,5 +124,7 @@ pub fn leftPanel(ctx: *WindowContext) void {
|
|||||||
dvui.label(@src(), "No document", .{}, .{});
|
dvui.label(@src(), "No document", .{}, .{});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
settings_section.deinit();
|
||||||
|
}
|
||||||
panel.deinit();
|
panel.deinit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Правая панель: контент документа (холст) или заглушка «Нет документа».
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const dvui = @import("dvui");
|
const dvui = @import("dvui");
|
||||||
const WindowContext = @import("../WindowContext.zig");
|
const WindowContext = @import("../WindowContext.zig");
|
||||||
@@ -6,6 +5,12 @@ const canvas_view = @import("canvas_view.zig");
|
|||||||
|
|
||||||
pub fn rightPanel(ctx: *WindowContext) void {
|
pub fn rightPanel(ctx: *WindowContext) void {
|
||||||
const fill_color = dvui.Color.black.opacity(0.25);
|
const fill_color = dvui.Color.black.opacity(0.25);
|
||||||
|
var back = dvui.box(
|
||||||
|
@src(),
|
||||||
|
.{ .dir = .horizontal },
|
||||||
|
.{ .expand = .both, .padding = dvui.Rect.all(12), .background = true },
|
||||||
|
);
|
||||||
|
{
|
||||||
var panel = dvui.box(
|
var panel = dvui.box(
|
||||||
@src(),
|
@src(),
|
||||||
.{ .dir = .vertical },
|
.{ .dir = .vertical },
|
||||||
@@ -28,12 +33,26 @@ pub fn rightPanel(ctx: *WindowContext) void {
|
|||||||
}
|
}
|
||||||
panel.deinit();
|
panel.deinit();
|
||||||
}
|
}
|
||||||
|
back.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
fn noDocView(ctx: *WindowContext) void {
|
fn noDocView(ctx: *WindowContext) void {
|
||||||
var center = dvui.box(
|
var center = dvui.box(
|
||||||
@src(),
|
@src(),
|
||||||
.{ .dir = .vertical },
|
.{ .dir = .vertical },
|
||||||
.{ .expand = .both, .padding = dvui.Rect.all(20) },
|
.{
|
||||||
|
.expand = .both,
|
||||||
|
.padding = dvui.Rect.all(20),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
{
|
||||||
|
var box = dvui.box(
|
||||||
|
@src(),
|
||||||
|
.{ .dir = .vertical },
|
||||||
|
.{
|
||||||
|
.gravity_x = 0.5,
|
||||||
|
.gravity_y = 0.5,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
{
|
{
|
||||||
dvui.label(@src(), "No document open", .{}, .{});
|
dvui.label(@src(), "No document open", .{}, .{});
|
||||||
@@ -43,5 +62,7 @@ fn noDocView(ctx: *WindowContext) void {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
box.deinit();
|
||||||
|
}
|
||||||
center.deinit();
|
center.deinit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Верхняя строка: вкладки документов + кнопка «Новый».
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const dvui = @import("dvui");
|
const dvui = @import("dvui");
|
||||||
const WindowContext = @import("../WindowContext.zig");
|
const WindowContext = @import("../WindowContext.zig");
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Отрисовка дочернего контента как текстуры с параметрами скругления
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const dvui = @import("dvui");
|
const dvui = @import("dvui");
|
||||||
const TexturedBox = @This();
|
const TexturedBox = @This();
|
||||||
@@ -26,10 +25,6 @@ pub fn deinit(self: *TexturedBox) void {
|
|||||||
const tex = dvui.textureFromTarget(picture.texture) catch null;
|
const tex = dvui.textureFromTarget(picture.texture) catch null;
|
||||||
if (tex) |t| {
|
if (tex) |t| {
|
||||||
dvui.Texture.destroyLater(t);
|
dvui.Texture.destroyLater(t);
|
||||||
// self.rs.r.y -= 2;
|
|
||||||
// self.rs.r.x -= 2;
|
|
||||||
// self.rs.r.h += 2;
|
|
||||||
// self.rs.r.w += 2;
|
|
||||||
dvui.renderTexture(t, self.rs, .{
|
dvui.renderTexture(t, self.rs, .{
|
||||||
.corner_radius = self.corner_radius,
|
.corner_radius = self.corner_radius,
|
||||||
}) catch {};
|
}) catch {};
|
||||||
|
|||||||
Reference in New Issue
Block a user