Убраны комментарии лишние и улучшены модели

This commit is contained in:
2026-02-23 23:01:49 +03:00
parent b896a67fd4
commit bd58286c98
15 changed files with 169 additions and 193 deletions

View File

@@ -19,13 +19,11 @@ scroll: dvui.ScrollInfo = .{
.horizontal = .auto,
},
native_scaling: bool = true,
/// Максимальная частота перерисовки при зуме. 0 = без ограничения.
redraw_throttle_ms: u32 = 50,
_visible_rect: ?ImageRect = null,
_zoom: f32 = 1,
_redraw_pending: bool = false,
_last_redraw_time_ms: i64 = 0,
/// Позиция курсора в координатах документа (обновляется в handleCanvasMouse). null если вне документа.
cursor_document_point: ?Point2 = null,
pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine) Canvas {
@@ -43,7 +41,6 @@ pub fn deinit(self: *Canvas) void {
}
}
/// Заполнить canvas градиентом
pub fn redrawExample(self: *Canvas) !void {
const full = self.getZoomedImageSize();
@@ -60,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;
if (new_texture) |tex| {
// Удалить старую текстуру
if (self.texture) |old_tex| {
dvui.Texture.destroyLater(old_tex);
}
@@ -70,7 +66,6 @@ pub fn redrawExample(self: *Canvas) !void {
self._last_redraw_time_ms = std.time.milliTimestamp();
}
// Ресетнуть example изображение в renderEngine
pub fn exampleReset(self: *Canvas) !void {
self.render_engine.exampleReset();
try self.redrawExample();
@@ -85,12 +80,10 @@ pub fn addZoom(self: *Canvas, value: f32) void {
self._zoom = @max(self._zoom, 0.01);
}
/// Запросить перерисовку (выполнится с учётом redraw_throttle_ms при вызове processPendingRedraw).
pub fn requestRedraw(self: *Canvas) void {
self._redraw_pending = true;
}
/// Выполнить отложенную перерисовку, если прошло не менее redraw_throttle_ms с прошлой отрисовки.
pub fn processPendingRedraw(self: *Canvas) !void {
if (!self._redraw_pending) return;
if (self.redraw_throttle_ms == 0) {
@@ -115,9 +108,6 @@ pub fn getZoomedImageSize(self: Canvas) ImageRect {
};
}
/// Перевести точку из системы координат контента скролла (natural scale, начало — левый верхний угол скроллируемой области)
/// в координаты документа (единицы документа до зума).
/// Возвращает null, если точка вне области изображения документа.
pub fn contentPointToDocument(self: Canvas, content_point: dvui.Point, natural_scale: f32) ?Point2 {
const img = self.getZoomedImageSize();
const left_n = @as(f32, @floatFromInt(img.x)) / natural_scale;
@@ -137,11 +127,6 @@ pub fn contentPointToDocument(self: Canvas, content_point: dvui.Point, natural_s
};
}
/// Обновить видимую часть изображения (в пикселях холста) и сохранить в `visible_rect`.
///
/// `viewport` и `scroll_offset` ожидаются в *physical* пикселях (т.е. уже умноженные на windowNaturalScale).
///
/// После обновления (или если текстуры ещё нет) перерисовывает текстуру, чтобы она содержала только видимую часть.
pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) bool {
const next = computeVisibleImageRect(self.*, viewport, scroll_offset);
var changed = false;
@@ -164,11 +149,9 @@ fn computeVisibleImageRect(self: Canvas, viewport: dvui.Rect, scroll_offset: dvu
const img_w: u32 = image_rect.w;
const img_h: u32 = image_rect.h;
// Видимый размер всегда равен размеру viewport, но не больше холста
const vis_w: u32 = @min(@as(u32, @intFromFloat(viewport.w)), img_w);
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_y: i64 = @intFromFloat(scroll_offset.y - @as(f32, @floatFromInt(image_rect.y)));

View File

@@ -7,13 +7,11 @@ const basic_models = @import("models/basic_models.zig");
const WindowContext = @This();
/// Один открытый документ: документ + свой холст и движок рендера
pub const OpenDocument = struct {
document: Document,
cpu_render: CpuRenderEngine,
canvas: Canvas,
/// Инициализировать по месту (canvas хранит указатель на cpu_render этого же экземпляра).
pub fn init(allocator: std.mem.Allocator, self: *OpenDocument) void {
const default_size = basic_models.Size{ .width = 800, .height = 600 };
self.document = Document.init(allocator, default_size);
@@ -29,9 +27,7 @@ pub const OpenDocument = struct {
allocator: std.mem.Allocator,
frame_index: u64,
/// Список открытых документов (вкладок): указатели на документ+холст
documents: std.ArrayList(*OpenDocument),
/// Индекс активной вкладки; null — ни один документ не выбран
active_document_index: ?usize,
pub fn init(allocator: std.mem.Allocator) !WindowContext {
@@ -54,14 +50,12 @@ pub fn deinit(self: *WindowContext) void {
self.documents.deinit(self.allocator);
}
/// Вернуть указатель на активный открытый документ (null если нет выбранной вкладки).
pub fn activeDocument(self: *WindowContext) ?*OpenDocument {
const i = self.active_document_index orelse return null;
if (i >= self.documents.items.len) return null;
return self.documents.items[i];
}
/// Добавить новый документ и сделать его активным.
pub fn addNewDocument(self: *WindowContext) !void {
const ptr = try self.allocator.create(OpenDocument);
errdefer self.allocator.destroy(ptr);
@@ -70,14 +64,12 @@ pub fn addNewDocument(self: *WindowContext) !void {
self.active_document_index = self.documents.items.len - 1;
}
/// Выбрать вкладку по индексу.
pub fn setActiveDocument(self: *WindowContext, index: usize) void {
if (index < self.documents.items.len) {
self.active_document_index = index;
}
}
/// Закрыть вкладку по индексу; активная вкладка сдвигается при необходимости.
pub fn closeDocument(self: *WindowContext, index: usize) void {
if (index >= self.documents.items.len) return;
const open_doc = self.documents.items[index];

View File

@@ -2,12 +2,14 @@ const std = @import("std");
const basic_models = @import("basic_models.zig");
const properties = @import("Property.zig");
const Property = properties.Property;
const PropertyData = properties.Data;
const Point2 = basic_models.Point2;
const Size = basic_models.Size;
const Object = @import("Object.zig");
const Document = @This();
size: Size,
allocator: std.mem.Allocator,
/// Корневые объекты документа (вложенность через Object.children).
objects: std.ArrayList(Object),
pub fn init(allocator: std.mem.Allocator, size: Size) Document {
@@ -23,112 +25,7 @@ pub fn deinit(self: *Document) void {
self.objects.deinit(self.allocator);
}
/// Добавить корневой объект в документ (клонирует в allocator документа).
pub fn addObject(self: *Document, template: Object) !void {
const obj = try template.clone(self.allocator);
try self.objects.append(self.allocator, obj);
}
/// Тип фигуры: определяет, как RenderEngine интерпретирует свойства и рисует объект.
pub const ShapeKind = enum {
rect,
ellipse,
line,
path,
};
/// Объект документа: тип фигуры, свойства и вложенные дочерние объекты.
/// Типы объектов задаются конструкторами; UI и RenderEngine работают с union Property.
pub const Object = struct {
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);
}
/// Добавить дочерний объект (клонирует в переданный allocator).
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);
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));
}
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);
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;
}
};

117
src/models/Object.zig Normal file
View File

@@ -0,0 +1,117 @@
const std = @import("std");
const basic_models = @import("basic_models.zig");
const defaultCommonProperties = @import("Property.zig").defaultCommonProperties;
const Property = @import("Property.zig").Property;
const PropertyData = @import("Property.zig").Data;
const Point2 = basic_models.Point2;
const Size = basic_models.Size;
const Object = @This();
pub const ShapeKind = enum {
rect,
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 createWithCommon(allocator: std.mem.Allocator, shape: ShapeKind) !Object {
const common = defaultCommonProperties();
var properties_list = std.ArrayList(Property).empty;
errdefer properties_list.deinit(allocator);
for (common) |d| try properties_list.append(allocator, .{ .data = d });
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, .{ .data = .{ .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, .{ .data = .{ .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, .{ .data = .{ .end_point = .{ .x = 100, .y = 0 } } });
return obj;
}
pub fn createBrokenLine(allocator: std.mem.Allocator) !Object {
var obj = try createWithCommon(allocator, .broken);
errdefer obj.deinit(allocator);
var points = std.ArrayList(Point2).init(allocator);
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;
}

View File

@@ -1,62 +1,73 @@
// Модель свойств объекта документа.
// Каждое свойство — отдельный тип в union; UI и RenderEngine работают с полиморфным Property.
// Комплексные значения (размер, радиусы) — один вариант свойства, а не несколько полей.
const std = @import("std");
const basic_models = @import("basic_models.zig");
const Point2 = basic_models.Point2;
const Scale2 = basic_models.Scale2;
const Size = basic_models.Size;
const Radii = basic_models.Radii;
/// Одно свойство объекта: полиморфный union.
/// Варианты — целостные значения (size, radii), не разбитые на отдельные поля.
/// UI перебирает свойства и по тегу показывает нужный редактор;
/// RenderEngine по тегу фигуры читает нужные свойства для отрисовки.
pub const Property = union(enum) {
// --- Общие для всех фигур (см. defaultCommonProperties) ---
/// Левый верхний угол bbox для rect/path; центр для ellipse; начало для line.
pub const Data = union(enum) {
position: Point2,
angle: f32,
scale: Scale2,
visible: bool,
opacity: f32,
locked: bool,
// --- Прямоугольник: один вариант ---
size: Size,
// --- Эллипс: один вариант ---
radii: Radii,
// --- Линия: конечная точка (относительно position) ---
end_point: Point2,
// --- Визуал (опционально для будущего) ---
points: std.ArrayList(Point2),
fill_rgba: u32,
stroke_rgba: u32,
};
const std = @import("std");
pub const Property = struct {
data: Data,
/// Общий набор свойств по умолчанию для любого объекта (одно место определения).
/// Конструкторы фигур добавляют сначала его, затем специфичные свойства.
pub fn defaultCommonProperties() []Property {
return .{
.{ .position = .{ .x = 0, .y = 0 } },
.{ .scale = .{ .scale_x = 1, .scale_y = 1 } },
.{ .visible = true },
.{ .opacity = 1.0 },
.{ .locked = false },
};
pub fn deinit(self: *Property, allocator: std.mem.Allocator) void {
switch (self.data) {
.points => |*list| list.deinit(allocator),
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: []const Data = .{
.{ .position = .{ .x = 0, .y = 0 } },
.{ .angle = 0 },
.{ .scale = .{ .scale_x = 1, .scale_y = 1 } },
.{ .visible = true },
.{ .opacity = 1.0 },
.{ .locked = false },
};
pub fn defaultCommonProperties() []const Data {
return &default_common_data;
}
test "Property is union" {
const p: Property = .{ .opacity = 0.5 };
try std.testing.expect(p == .opacity);
try std.testing.expect(p.opacity == 0.5);
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" {
const common = defaultCommonProperties();
try std.testing.expect(common[0] == .position);
try std.testing.expect(common[0].position.x == 0);
try std.testing.expect(common[2].visible == true);
}

View File

@@ -15,19 +15,16 @@ pub const Size = struct {
height: f32,
};
/// Точка в 2D (документные единицы)
pub const Point2 = struct {
x: f32 = 0,
y: f32 = 0,
};
/// Радиусы эллипса по осям (одно свойство).
pub const Radii = struct {
x: f32,
y: f32,
};
/// Масштаб объекта
pub const Scale2 = struct {
scale_x: f32 = 1,
scale_y: f32 = 1,

View File

@@ -27,7 +27,6 @@ pub fn init(allocator: Allocator, render_type: Type) CpuRenderEngine {
}
pub fn exampleReset(self: *CpuRenderEngine) void {
// Сгенерировать случайные цвета градиента
var prng = std.Random.DefaultPrng.init(@intCast(std.time.microTimestamp()));
const random = prng.random();
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;
const colors = [_]Color.PMA{
.{ .r = 255, .g = 0, .b = 0, .a = 255 }, // red
.{ .r = 255, .g = 165, .b = 0, .a = 255 }, // orange
.{ .r = 255, .g = 255, .b = 0, .a = 255 }, // yellow
.{ .r = 0, .g = 255, .b = 0, .a = 255 }, // green
.{ .r = 0, .g = 255, .b = 255, .a = 255 }, // cyan
.{ .r = 0, .g = 0, .b = 255, .a = 255 }, // blue
.{ .r = 255, .g = 0, .b = 0, .a = 255 },
.{ .r = 255, .g = 165, .b = 0, .a = 255 },
.{ .r = 255, .g = 255, .b = 0, .a = 255 },
.{ .r = 0, .g = 255, .b = 0, .a = 255 },
.{ .r = 0, .g = 255, .b = 255, .a = 255 },
.{ .r = 0, .g = 0, .b = 255, .a = 255 },
};
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;
if (canvas_x >= canvas_size.w) continue;
// Check vertical line index
var vertical_index: ?u32 = null;
for (0..x_pos.len) |i| {
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;
for (0..y_pos.len) |i| {
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| {
pixels[y * visible_rect.w + x] = colors[idx];
} else {
// Find square
var square_x: u32 = 0;
for (0..squares_num) |i| {
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 height = visible_rect.h;
// Выделить буфер пиксельных данных
const pixels = try self._allocator.alloc(Color.PMA, @as(usize, width) * height);
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) {
.Gradient => self.renderGradient(pixels, width, height, full_w, full_h, visible_rect),
.Squares => self.renderSquares(pixels, canvas_size, visible_rect),

View File

@@ -1,4 +1,3 @@
// Интерфейс для рендеринга документа
const dvui = @import("dvui");
const CpuRenderEngine = @import("CpuRenderEngine.zig");
const basic_models = @import("../models/basic_models.zig");

View File

@@ -1,4 +1,3 @@
// Виджет холста: скролл, текстура, зум по Ctrl+колёсико.
const std = @import("std");
const dvui = @import("dvui");
const dvui_ext = @import("dvui_ext.zig");
@@ -120,7 +119,6 @@ fn handleCanvasZoom(canvas: *Canvas, scroll: anytype) void {
}
}
/// Обрабатывает события мыши: переводит позицию курсора в координаты документа и сохраняет в canvas.cursor_document_point.
fn handleCanvasMouse(canvas: *Canvas, scroll: anytype) void {
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();

View File

@@ -1,4 +1,3 @@
// Расширения для dvui
const std = @import("std");
const dvui = @import("dvui");
const TexturedBox = @import("./types/TexturedBox.zig");

View File

@@ -1,11 +1,9 @@
// Корневой кадр 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;

View File

@@ -1,4 +1,3 @@
// Левая панель: инструменты для активного документа (scaling, тип рендера).
const dvui = @import("dvui");
const WindowContext = @import("../WindowContext.zig");

View File

@@ -1,4 +1,3 @@
// Правая панель: контент документа (холст) или заглушка «Нет документа».
const std = @import("std");
const dvui = @import("dvui");
const WindowContext = @import("../WindowContext.zig");

View File

@@ -1,4 +1,3 @@
// Верхняя строка: вкладки документов + кнопка «Новый».
const std = @import("std");
const dvui = @import("dvui");
const WindowContext = @import("../WindowContext.zig");

View File

@@ -1,4 +1,3 @@
// Отрисовка дочернего контента как текстуры с параметрами скругления
const std = @import("std");
const dvui = @import("dvui");
const TexturedBox = @This();
@@ -26,10 +25,6 @@ pub fn deinit(self: *TexturedBox) void {
const tex = dvui.textureFromTarget(picture.texture) catch null;
if (tex) |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, .{
.corner_radius = self.corner_radius,
}) catch {};