From 643aaee926d2bc3241da3cfd79b61509e5af218a Mon Sep 17 00:00:00 2001 From: Roman Pytkov Date: Sat, 20 Dec 2025 20:47:37 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BA=D0=B5=D0=B9=D0=BB=D0=B8=D0=BD?= =?UTF-8?q?=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Canvas.zig | 75 +++++++++++++++++++++++++++-------- src/main.zig | 103 +++++++++++++++++++++++++++++++++---------------- 2 files changed, 127 insertions(+), 51 deletions(-) diff --git a/src/Canvas.zig b/src/Canvas.zig index 319eb92..811098b 100644 --- a/src/Canvas.zig +++ b/src/Canvas.zig @@ -15,14 +15,15 @@ pub const ImageRect = struct { allocator: std.mem.Allocator, texture: ?dvui.Texture = null, +visible_rect: ?ImageRect = null, size: Size = .{ .w = 800, .h = 600 }, pos: dvui.Point = dvui.Point{ .x = 0, .y = 0 }, scroll: dvui.ScrollInfo = .{ .vertical = .auto, .horizontal = .auto, - // .virtual_size = .{ .w = 2000, .h = 2000 }, }, zoom: f32 = 1, +native_scaling: bool = false, gradient_start: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 }, gradient_end: Color.PMA = .{ .r = 255, .g = 255, .b = 255, .a = 255 }, @@ -39,9 +40,26 @@ pub fn deinit(self: *Canvas) void { /// Заполнить canvas градиентом pub fn redrawGradient(self: *Canvas) !void { - const size = self.getScaledImageSize(); - const width: u32 = size.w; - const height: u32 = size.h; + const full = self.getScaledImageSize(); + const full_w: u32 = full.w; + const full_h: u32 = full.h; + + var vis: ImageRect = self.visible_rect orelse ImageRect{ .x = 0, .y = 0, .w = 0, .h = 0 }; + if (vis.w == 0 or vis.h == 0) { + // Если viewport ещё не известен, рисуем целиком. + vis = .{ .x = 0, .y = 0, .w = full_w, .h = full_h }; + } + + if (vis.w == 0 or vis.h == 0) { + if (self.texture) |tex| { + dvui.Texture.destroyLater(tex); + self.texture = null; + } + return; + } + + const width: u32 = vis.w; + const height: u32 = vis.h; // Выделить буфер пиксельных данных const pixels = try self.allocator.alloc(Color.PMA, @as(usize, width) * height); @@ -51,10 +69,22 @@ pub fn redrawGradient(self: *Canvas) !void { while (y < height) : (y += 1) { var x: u32 = 0; while (x < width) : (x += 1) { - const factor = (@as(f32, @floatFromInt(x)) / @as(f32, @floatFromInt(width - 1)) + @as(f32, @floatFromInt(y)) / @as(f32, @floatFromInt(height - 1))) / 2; - const r = @as(u8, @intFromFloat(@as(f32, @floatFromInt(self.gradient_start.r)) + factor * (@as(f32, @floatFromInt(self.gradient_end.r)) - @as(f32, @floatFromInt(self.gradient_start.r))))); - const g = @as(u8, @intFromFloat(@as(f32, @floatFromInt(self.gradient_start.g)) + factor * (@as(f32, @floatFromInt(self.gradient_end.g)) - @as(f32, @floatFromInt(self.gradient_start.g))))); - const b = @as(u8, @intFromFloat(@as(f32, @floatFromInt(self.gradient_start.b)) + factor * (@as(f32, @floatFromInt(self.gradient_end.b)) - @as(f32, @floatFromInt(self.gradient_start.b))))); + const gx: u32 = vis.x + x; + const gy: u32 = vis.y + y; + + const denom_x: f32 = if (full_w > 1) @as(f32, @floatFromInt(full_w - 1)) else 1; + const denom_y: f32 = if (full_h > 1) @as(f32, @floatFromInt(full_h - 1)) else 1; + const fx: f32 = @as(f32, @floatFromInt(gx)) / denom_x; + const fy: f32 = @as(f32, @floatFromInt(gy)) / denom_y; + const factor: f32 = std.math.clamp((fx + fy) / 2, 0, 1); + + const r_f: f32 = @as(f32, @floatFromInt(self.gradient_start.r)) + factor * (@as(f32, @floatFromInt(self.gradient_end.r)) - @as(f32, @floatFromInt(self.gradient_start.r))); + const g_f: f32 = @as(f32, @floatFromInt(self.gradient_start.g)) + factor * (@as(f32, @floatFromInt(self.gradient_end.g)) - @as(f32, @floatFromInt(self.gradient_start.g))); + const b_f: f32 = @as(f32, @floatFromInt(self.gradient_start.b)) + factor * (@as(f32, @floatFromInt(self.gradient_end.b)) - @as(f32, @floatFromInt(self.gradient_start.b))); + + const r: u8 = @intFromFloat(std.math.clamp(r_f, 0, 255)); + const g: u8 = @intFromFloat(std.math.clamp(g_f, 0, 255)); + const b: u8 = @intFromFloat(std.math.clamp(b_f, 0, 255)); pixels[y * width + x] = .{ .r = r, .g = g, .b = b, .a = 255 }; } } @@ -93,15 +123,26 @@ pub fn getScaledImageSize(self: Canvas) ImageRect { }; } -/// Видимая часть изображения, которую реально видит пользователь. +/// Обновить видимую часть изображения (в пикселях изображения) и сохранить в `visible_rect`. /// -/// Возвращает прямоугольник в координатах изображения (пиксели). +/// `viewport` и `scroll_offset` ожидаются в *physical* пикселях (т.е. уже умноженные на windowNaturalScale). /// -/// Важно: функция НЕ зависит от `texture` и может вызываться до её создания. -/// Параметры: -/// - `viewport`: размер видимой области scroll-area (x/y игнорируются) -/// - `scroll_offset`: текущий scroll (виртуальные координаты контента) -pub fn getVisibleImageRect(self: Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) ImageRect { +/// После обновления (или если текстуры ещё нет) перерисовывает текстуру, чтобы она содержала только видимую часть. +pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) !void { + const next = computeVisibleImageRect(self.*, viewport, scroll_offset); + var changed = false; + if (self.visible_rect) |vis| { + changed |= next.x != vis.x or next.y != vis.y or next.w != vis.w or next.h != vis.h; + } + self.visible_rect = next; + std.debug.print("Visible: {any}\n", .{next}); + + if (changed or self.texture == null) { + try self.redrawGradient(); + } +} + +fn computeVisibleImageRect(self: Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) ImageRect { const image_rect = self.getScaledImageSize(); const img_w_f: f32 = @floatFromInt(image_rect.w); @@ -160,8 +201,8 @@ fn floatToClampedU32(value: f32, max_inclusive: u32) u32 { } /// Отобразить canvas в UI -pub fn render(self: Canvas, rect: dvui.Rect.Physical) !void { +pub fn render(self: Canvas, rect: dvui.RectScale) !void { if (self.texture) |texture| { - try dvui.renderTexture(texture, .{ .r = rect }, .{}); + try dvui.renderTexture(texture, rect, .{}); } } diff --git a/src/main.zig b/src/main.zig index b86c83b..f343d7a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -93,9 +93,10 @@ fn gui_frame(ctx: *WindowContext) bool { canvas.fillRandomGradient() catch |err| { std.debug.print("Error filling canvas: {}\n", .{err}); }; - canvas.pos = .{ .x = 800, .y = 400 }; + canvas.pos = .{ .x = 400, .y = 400 }; canvas.zoom = dvui.windowNaturalScale(); } + if (dvui.checkbox(@src(), &canvas.native_scaling, "Scaling", .{})) {} } left_panel.deinit(); @@ -106,7 +107,7 @@ fn gui_frame(ctx: *WindowContext) bool { .{ .expand = .both, .padding = dvui.Rect.all(12), .background = true }, ); { - const fill_color = Color.white.opacity(0.5); + const fill_color = Color.black.opacity(0.25); var right_panel = dvui.box( @src(), .{ .dir = .vertical }, @@ -139,45 +140,78 @@ fn gui_frame(ctx: *WindowContext) bool { }, ); { - // Отобразить canvas внутри scroll area. - // ScrollArea сам двигает дочерние виджеты, поэтому margin не нужен. - if (canvas.texture) |texture| { - const natural_scale = dvui.windowNaturalScale(); - const img_size = canvas.getScaledImageSize(); - _ = dvui.image(@src(), .{ - .source = .{ .texture = texture }, - }, .{ + // Canvas область внутри scroll area (полный размер изображения). + // Важно: мы НЕ двигаем отдельный image-виджет под visible_rect; + // вместо этого рисуем частичную текстуру внутри этой области со сдвигом. + const natural_scale = dvui.windowNaturalScale(); + const img_area_divider = if (canvas.native_scaling) 1 else natural_scale; + const img_size = canvas.getScaledImageSize(); + + var img_area = dvui.box( + @src(), + .{ .dir = .vertical }, + .{ + .background = false, .margin = .{ - // img_size.* считаем в пикселях картинки (physical). - // dvui.image ожидает размеры в natural, поэтому делим на scale. - .x = @as(f32, @floatFromInt(img_size.x)) / natural_scale, - .y = @as(f32, @floatFromInt(img_size.y)) / natural_scale, + .x = @as(f32, @floatFromInt(img_size.x)) / img_area_divider, + .y = @as(f32, @floatFromInt(img_size.y)) / img_area_divider, + .w = @as(f32, @floatFromInt(img_size.x)) / img_area_divider, + .h = @as(f32, @floatFromInt(img_size.y)) / img_area_divider, }, .min_size_content = .{ - .w = @as(f32, @floatFromInt(img_size.w)) / natural_scale, - .h = @as(f32, @floatFromInt(img_size.h)) / natural_scale, + .w = @as(f32, @floatFromInt(img_size.w)) / img_area_divider, + .h = @as(f32, @floatFromInt(img_size.h)) / img_area_divider, }, - }); + .max_size_content = .{ + .w = @as(f32, @floatFromInt(img_size.w)) / img_area_divider, + .h = @as(f32, @floatFromInt(img_size.h)) / img_area_divider, + }, + }, + ); + defer img_area.deinit(); - // Получить 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 и 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, - }; + // viewport_rect/scroll_current — в natural единицах. + // Для расчёта видимой области в пикселях изображения переводим в physical. + const viewport_px = dvui.Rect{ + .x = viewport_rect.x * img_area_divider, + .y = viewport_rect.y * img_area_divider, + .w = viewport_rect.w * img_area_divider, + .h = viewport_rect.h * img_area_divider, + }; + const scroll_px = dvui.Point{ + .x = scroll_current.x * img_area_divider, + .y = scroll_current.y * img_area_divider, + }; - const visible_rect = canvas.getVisibleImageRect(viewport_px, scroll_px); - std.debug.print("Visible image rect: {any}\n", .{visible_rect}); + canvas.updateVisibleImageRect(viewport_px, scroll_px) catch |err| { + std.debug.print("updateVisibleImageRect error: {}\n", .{err}); + }; + + // if (canvas.visible_rect) |vis| { + // if (vis.w != 0 and vis.h != 0) { + // const area_rs = img_area.data().contentRectScale(); + // var part_rs = area_rs; + // part_rs.r = .{ + // .x = area_rs.r.x + @as(f32, @floatFromInt(vis.x)), + // .y = area_rs.r.y + @as(f32, @floatFromInt(vis.y)), + // .w = @as(f32, @floatFromInt(vis.w)), + // .h = @as(f32, @floatFromInt(vis.h)), + // }; + // canvas.render(part_rs) catch {}; + // } + // } + if (canvas.texture) |tex| { + _ = dvui.image( + @src(), + .{ .source = .{ .texture = tex } }, + .{ + .expand = .both, + }, + ); } // Заблокировать события скролла, если нажат ctrl @@ -189,6 +223,7 @@ fn gui_frame(ctx: *WindowContext) bool { if (dvui.eventMatchSimple(e, scroll.data()) and (action == .wheel_x or action == .wheel_y)) { switch (action) { .wheel_y => |y| { + canvas.updateVisibleImageRect(viewport_px, scroll_px) catch {}; canvas.addZoom(y / 1000); canvas.redrawGradient() catch {}; },