diff --git a/src/Surface.zig b/src/Surface.zig index 0975c46..8a23ea8 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -590,10 +590,8 @@ const fallback_palette: [16]vaxis.Cell.Color = .{ .{ .rgb = .{ 0xff, 0xff, 0xff } }, // Bright White }; -const DIM_UNFOCUSED: f32 = 0.05; - -pub fn render(self: *const Surface, win: vaxis.Window, focused: bool) void { - const dim_factor: f32 = if (focused) 0.0 else DIM_UNFOCUSED; +pub fn render(self: *const Surface, win: vaxis.Window, focused: bool, colors: ?*const TerminalColors, dim_factor: f32) void { + _ = colors; // Colors are stored in self.colors for (0..self.rows) |row| { for (0..self.cols) |col| { @@ -620,6 +618,9 @@ pub fn render(self: *const Surface, win: vaxis.Window, focused: bool) void { } fn applyDimming(self: *const Surface, cell: *vaxis.Cell, dim_factor: f32) bool { + // Skip dimming for default backgrounds to preserve transparency + if (cell.style.bg == .default) return false; + const bg_rgb = self.resolveBgColor(cell.style.bg) orelse return false; const dimmed_bg = TerminalColors.reduceContrast(bg_rgb, dim_factor); cell.style.bg = .{ .rgb = dimmed_bg }; @@ -630,7 +631,7 @@ fn resolveBgColor(self: *const Surface, bg: vaxis.Cell.Color) ?[3]u8 { return switch (bg) { .rgb => |rgb| rgb, .index => |idx| self.resolvePaletteColor(idx), - .default => self.resolveDefaultBg(), + .default => null, // Don't resolve default - preserve transparency }; } diff --git a/src/client.zig b/src/client.zig index 44e20c0..d188d09 100644 --- a/src/client.zig +++ b/src/client.zig @@ -1537,8 +1537,149 @@ pub const App = struct { } } + fn applyDimToStyle(self: *App, style: vaxis.Style, dim_factor: f32) vaxis.Style { + if (dim_factor == 0.0) return style; + + var dimmed = style; + + // Apply dimming to foreground color + if (dimmed.fg != .default) { + const fg_rgb = self.resolveColor(dimmed.fg); + if (fg_rgb) |rgb| { + dimmed.fg = .{ .rgb = Surface.TerminalColors.reduceContrast(rgb, dim_factor * 0.5) }; + } + } + + // Apply dimming to background color + if (dimmed.bg != .default) { + const bg_rgb = self.resolveColor(dimmed.bg); + if (bg_rgb) |rgb| { + dimmed.bg = .{ .rgb = Surface.TerminalColors.reduceContrast(rgb, dim_factor) }; + } + } + + return dimmed; + } + + fn resolveColor(self: *App, color: vaxis.Cell.Color) ?[3]u8 { + return switch (color) { + .rgb => |rgb| rgb, + .index => |idx| blk: { + if (idx >= 16) break :blk null; + if (self.colors.palette[idx]) |c_val| { + break :blk switch (c_val) { + .rgb => |rgb| rgb, + else => null, + }; + } + break :blk null; + }, + .default => blk: { + if (self.colors.bg) |bg| { + break :blk switch (bg) { + .rgb => |rgb| rgb, + else => null, + }; + } + break :blk null; + }, + }; + } + fn renderWidget(self: *App, w: widget.Widget, win: vaxis.Window) !void { - try w.renderTo(win, self.allocator); + switch (w.kind) { + .surface => |surf| { + if (self.surfaces.get(surf.pty_id)) |surface| { + // Check for resize mismatch and send resize request if needed + if (surface.rows != w.height or surface.cols != w.width) { + log.debug("renderWidget: surface resize needed for pty {}: {}x{} -> {}x{}", .{ + surf.pty_id, + surface.cols, + surface.rows, + w.width, + w.height, + }); + self.sendResize(surf.pty_id, w.height, w.width) catch |err| { + log.err("Failed to send resize: {}", .{err}); + }; + } + // Calculate dim factor based on focus + const dim_factor: f32 = if (w.focus) 0.0 else self.ui.dim_factor; + surface.render(win, w.focus, &self.colors, dim_factor); + } + }, + .text => { + // Use renderTo for text widgets + try w.renderTo(win, self.allocator); + }, + .box => |b| { + const chars = b.borderChars(); + // Apply dimming to border style if not focused + const dim_factor: f32 = if (w.focus) 0.0 else self.ui.dim_factor; + const style = self.applyDimToStyle(b.style, dim_factor); + + log.debug("render box: w={} h={} focus={} dim_factor={d}", .{ win.width, win.height, w.focus, dim_factor }); + + // Fill background for the entire box area + if (style.bg != .default) { + for (0..win.height) |row| { + for (0..win.width) |col| { + win.writeCell(@intCast(col), @intCast(row), .{ + .char = .{ .grapheme = " ", .width = 1 }, + .style = style, + }); + } + } + } + + // Render border if not none + if (b.border != .none and win.width >= 2 and win.height >= 2) { + win.writeCell(0, 0, .{ .char = .{ .grapheme = chars.tl, .width = 1 }, .style = style }); + win.writeCell(win.width - 1, 0, .{ .char = .{ .grapheme = chars.tr, .width = 1 }, .style = style }); + win.writeCell(0, win.height - 1, .{ .char = .{ .grapheme = chars.bl, .width = 1 }, .style = style }); + win.writeCell(win.width - 1, win.height - 1, .{ .char = .{ .grapheme = chars.br, .width = 1 }, .style = style }); + + for (1..win.width - 1) |col| { + win.writeCell(@intCast(col), 0, .{ .char = .{ .grapheme = chars.h, .width = 1 }, .style = style }); + win.writeCell(@intCast(col), win.height - 1, .{ .char = .{ .grapheme = chars.h, .width = 1 }, .style = style }); + } + + for (1..win.height - 1) |row| { + win.writeCell(0, @intCast(row), .{ .char = .{ .grapheme = chars.v, .width = 1 }, .style = style }); + win.writeCell(win.width - 1, @intCast(row), .{ .char = .{ .grapheme = chars.v, .width = 1 }, .style = style }); + } + } + + // Render child widget if present + const inner_win = win.child(.{ + .x_off = if (b.border != .none) 1 else 0, + .y_off = if (b.border != .none) 1 else 0, + .width = if (b.border != .none and win.width >= 2) win.width - 2 else win.width, + .height = if (b.border != .none and win.height >= 2) win.height - 2 else win.height, + }); + try self.renderWidget(b.child.*, inner_win); + }, + .positioned => |pos| { + const child_win = win.child(.{ + .x_off = pos.x orelse 0, + .y_off = pos.y orelse 0, + .width = pos.child.width, + .height = pos.child.height, + }); + try self.renderWidget(pos.child.*, child_win); + }, + .text_input, + .list, + .padding, + .column, + .row, + .stack, + .separator, + => { + // These widgets use renderTo for now + try w.renderTo(win, self.allocator); + }, + } } pub fn scheduleRender(self: *App) !void { diff --git a/src/ui.zig b/src/ui.zig index e988948..fef60a4 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -87,6 +87,7 @@ pub const UI = struct { get_session_name_ctx: *anyopaque = undefined, rename_session_callback: ?*const fn (ctx: *anyopaque, new_name: []const u8) anyerror!void = null, rename_session_ctx: *anyopaque = undefined, + dim_factor: f32 = 0.025, text_inputs: std.AutoHashMap(u32, *TextInput), next_text_input_id: u32 = 1, @@ -215,6 +216,19 @@ pub const UI = struct { // Store pointer to self in registry for static functions to use self.lua.pushLightUserdata(self); self.lua.setField(ziglua.registry_index, "prise_ui_ptr"); + + // Apply any pending dim_factor that was set during init.lua + _ = self.lua.getField(ziglua.registry_index, "_pending_dim_factor"); + if (self.lua.typeOf(-1) == .number) { + const pending_dim = self.lua.toNumber(-1) catch 0.025; + log.debug("setLoop: applying pending dim_factor={d}", .{pending_dim}); + self.dim_factor = @floatCast(pending_dim); + self.lua.pop(1); + self.lua.pushNil(); + self.lua.setField(ziglua.registry_index, "_pending_dim_factor"); + } else { + self.lua.pop(1); + } } pub fn setExitCallback(self: *UI, ctx: *anyopaque, cb: *const fn (ctx: *anyopaque) void) void { @@ -375,6 +389,14 @@ pub const UI = struct { lua.pushFunction(ziglua.wrap(renameSession)); lua.setField(-2, "rename_session"); + // Register set_dim_factor + lua.pushFunction(ziglua.wrap(setDimFactor)); + lua.setField(-2, "set_dim_factor"); + + // Register get_dim_factor + lua.pushFunction(ziglua.wrap(getDimFactor)); + lua.setField(-2, "get_dim_factor"); + // Register create_text_input lua.pushFunction(ziglua.wrap(createTextInput)); lua.setField(-2, "create_text_input"); @@ -639,6 +661,43 @@ pub const UI = struct { return 1; } + fn getDimFactor(lua: *ziglua.Lua) i32 { + _ = lua.getField(ziglua.registry_index, "prise_ui_ptr"); + const ui_ptr = lua.toPointer(-1) catch { + lua.pushNumber(0.025); + return 1; + }; + lua.pop(1); + + const ui: *UI = @ptrCast(@alignCast(@constCast(ui_ptr))); + lua.pushNumber(ui.dim_factor); + return 1; + } + + fn setDimFactor(lua: *ziglua.Lua) i32 { + const dim: f64 = lua.toNumber(1) catch 0.025; + + // Try to get the UI pointer and set it directly + _ = lua.getField(ziglua.registry_index, "prise_ui_ptr"); + const ui_ptr_result = lua.toPointer(-1); + lua.pop(1); + + if (ui_ptr_result) |ui_ptr| { + const ui: *UI = @ptrCast(@alignCast(@constCast(ui_ptr))); + log.debug("setDimFactor: setting dim_factor to {d}", .{dim}); + ui.dim_factor = @floatCast(dim); + lua.pushBoolean(true); + return 1; + } else |_| { + // UI pointer not available yet (we're in init.lua), store the value for later + log.debug("setDimFactor: storing pending dim_factor={d}", .{dim}); + lua.pushNumber(dim); + lua.setField(ziglua.registry_index, "_pending_dim_factor"); + lua.pushBoolean(true); + return 1; + } + } + fn detach(lua: *ziglua.Lua) i32 { _ = lua.getField(ziglua.registry_index, "prise_ui_ptr"); const ui = lua.toUserdata(UI, -1) catch { diff --git a/src/widget.zig b/src/widget.zig index ced7375..b23d057 100644 --- a/src/widget.zig +++ b/src/widget.zig @@ -252,7 +252,7 @@ pub const Widget = struct { pub fn renderTo(self: *const Widget, win: vaxis.Window, allocator: std.mem.Allocator) !void { switch (self.kind) { .surface => |surf| { - surf.surface.render(win, self.focus); + surf.surface.render(win, self.focus, null, 0.0); }, .text_input => |ti| { ti.input.updateScrollOffset(@intCast(win.width));