diff --git a/docs/changelog.rst b/docs/changelog.rst index 7424aab65..b951be116 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -184,6 +184,8 @@ Detailed list of changes - Add a new :code:`kitten @ screenshot` remote control command to take a pixel perfect PNG screenshot of an OS Window, tab or window +- A new option, :opt:`padding_fill_strategy` to control how the thin padding strips that appear when the window size is not an exact multiple of the cell size are colored. You can choose to have the padding colored to match the background of each neighboring cell, effectively extending the size of the cell or you can continue to use the existing behavior of using the background. + 0.48.1 [2026-07-24] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/data-types.h b/kitty/data-types.h index 89b35d28b..459c9709c 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -119,6 +119,7 @@ typedef enum { SCROLLBAR_NEVER, SCROLLBAR_ON_SCROLLED, SCROLLBAR_ON_HOVERED, SCR typedef enum { PROGRESS_BAR_HIDDEN, PROGRESS_BAR_LEFT, PROGRESS_BAR_RIGHT, PROGRESS_BAR_TOP, PROGRESS_BAR_BOTTOM } ProgressBarPosition; typedef enum { PROGRESS_STATE_UNSET, PROGRESS_STATE_SET, PROGRESS_STATE_ERROR, PROGRESS_STATE_INDETERMINATE, PROGRESS_STATE_PAUSED } ProgressBarState; typedef enum { TILING, SCALED, MIRRORED, CLAMPED, CENTER_CLAMPED, CENTER_SCALED } BackgroundImageLayout; +typedef enum { PADDING_FILL_BACKGROUND, PADDING_FILL_NEIGHBORING_CELL } PaddingFillStrategy; typedef struct ImageAnchorPosition { float canvas_x, canvas_y, image_x, image_y; } ImageAnchorPosition; diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index b1ec1c58d..9f458885b 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -281,6 +281,7 @@ CURSOR_UNDERLINE: int DECAWM: int BGIMAGE_PROGRAM: int CELL_PROGRAM: int +PADDING_PROGRAM: int CELL_FG_PROGRAM: int CELL_BG_PROGRAM: int BLIT_PROGRAM: int @@ -1426,7 +1427,8 @@ def set_window_title_bar_render_data( def set_window_render_data( os_window_id: int, tab_id: int, window_id: int, screen: Screen, left: int, top: int, right: int, bottom: int, - spaces_left: int, spaces_top: int, spaces_right: int, spaces_bottom: int + spaces_left: int, spaces_top: int, spaces_right: int, spaces_bottom: int, + cp_left: int, cp_top: int, cp_right: int, cp_bottom: int, ) -> None: pass diff --git a/kitty/gl.c b/kitty/gl.c index 6b0bf3e13..1384ab6ac 100644 --- a/kitty/gl.c +++ b/kitty/gl.c @@ -595,6 +595,32 @@ add_attribute_to_vao(ssize_t vao_idx, int location, GLint size, GLenum data_type add_located_attribute_to_vao(vao_idx, location, size, data_type, stride, offset, divisor); } +void +set_vao_attribute(ssize_t vao_idx, size_t buffer_idx, int location, GLint size, GLenum data_type, GLsizei stride, void *offset, GLuint divisor) { + // (Re)configure an attribute pointer that reads from a specific buffer of + // the VAO (unlike add_attribute_to_vao which always uses the last added + // buffer). The VAO must be bound before calling this. + VAO *vao = vaos + vao_idx; + ssize_t buf = vao->buffers[buffer_idx]; + bind_buffer(buf); + glEnableVertexAttribArray(location); + switch(data_type) { + case GL_BYTE: + case GL_UNSIGNED_BYTE: + case GL_SHORT: + case GL_UNSIGNED_SHORT: + case GL_INT: + case GL_UNSIGNED_INT: + glVertexAttribIPointer(location, size, data_type, stride, offset); + break; + default: + glVertexAttribPointer(location, size, data_type, GL_FALSE, stride, offset); + break; + } + glVertexAttribDivisorARB(location, divisor); + unbind_buffer(buf); +} + void remove_vao(ssize_t vao_idx) { VAO *vao = vaos + vao_idx; diff --git a/kitty/gl.h b/kitty/gl.h index 55f97fe12..a5e4cd5c0 100644 --- a/kitty/gl.h +++ b/kitty/gl.h @@ -66,6 +66,7 @@ ArrayInformation program_uniform_array(int program, const char *name); ssize_t create_vao(void); size_t add_buffer_to_vao(ssize_t vao_idx, GLenum usage); void add_attribute_to_vao(ssize_t vao_idx, int location, GLint size, GLenum data_type, GLsizei stride, void *offset, GLuint divisor); +void set_vao_attribute(ssize_t vao_idx, size_t buffer_idx, int location, GLint size, GLenum data_type, GLsizei stride, void *offset, GLuint divisor); ssize_t alloc_vao_buffer(ssize_t vao_idx, GLsizeiptr size, size_t bufnum, GLenum usage); void* alloc_and_map_vao_buffer(ssize_t vao_idx, GLsizeiptr size, size_t bufnum, bool frequently_updated); void unmap_vao_buffer(ssize_t vao_idx, size_t bufnum); diff --git a/kitty/layout/base.py b/kitty/layout/base.py index 6d9be87c8..3882848c9 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -37,6 +37,11 @@ class LayoutData(NamedTuple): space_before: int = 0 space_after: int = 0 content_size: int = 0 + # The portion of space_before/space_after that is compensatory padding + # arising from the window size not being an exact multiple of the cell size + # (as opposed to the intentional margin/border/padding decoration). + compensatory_before: int = 0 + compensatory_after: int = 0 DecorationPairs = Sequence[tuple[int, int]] @@ -47,6 +52,7 @@ ListOfWindows = list[WindowType] class LayoutGlobalData: draw_minimal_borders: bool = True draw_active_borders: bool = True + fill_padding_with_neighboring_cell: bool = False alignment_x: int = 0 alignment_y: int = 0 @@ -75,6 +81,7 @@ def effective_draw_minimal_borders(opts: Options, has_more_than_one_visible_grou def set_layout_options(opts: Options) -> None: lgd.draw_minimal_borders = effective_draw_minimal_borders(opts) lgd.draw_active_borders = opts.active_border_color is not None + lgd.fill_padding_with_neighboring_cell = opts.padding_fill_strategy == 'neighboring_cell' lgd.alignment_x = -1 if opts.placement_strategy.endswith('left') else 1 if opts.placement_strategy.endswith('right') else 0 lgd.alignment_y = -1 if opts.placement_strategy.startswith('top') else 1 if opts.placement_strategy.startswith('bottom') else 0 @@ -148,7 +155,12 @@ def layout_dimension( after_space = (start_at + length) - (pos + content_size) else: after_space = after_dec - yield LayoutData(pos, cells_per_window, before_space, after_space, content_size) + # Whatever space is present beyond the intentional decoration is the + # compensatory padding from the size mismatch (only ever non-zero for + # the first/last windows, which absorb the leading/trailing remainder). + yield LayoutData( + pos, cells_per_window, before_space, after_space, content_size, + before_space - before_dec, after_space - after_dec) pos += content_size + after_space @@ -160,28 +172,50 @@ class Rect(NamedTuple): def blank_rects_for_window(wg: WindowGeometry) -> Generator[Rect, None, None]: - left_width, right_width = wg.spaces.left, wg.spaces.right - top_height, bottom_height = wg.spaces.top, wg.spaces.bottom + left, top, right, bottom = wg.left, wg.top, wg.right, wg.bottom + left_width, top_height, right_width, bottom_height = wg.spaces + if lgd.fill_padding_with_neighboring_cell: + # The compensatory padding is the innermost slice of the spaces, adjacent + # to the cells. It is drawn separately by the padding shader so that it + # matches its neighboring cell. Exclude it here by expanding the content + # box over it and only painting the remaining outer decoration frame in + # the background color. + c = wg.compensatory + left -= c.left + top -= c.top + right += c.right + bottom += c.bottom + left_width -= c.left + top_height -= c.top + right_width -= c.right + bottom_height -= c.bottom if left_width > 0: - yield Rect(wg.left - left_width, wg.top - top_height, wg.left, wg.bottom + bottom_height) + yield Rect(left - left_width, top - top_height, left, bottom + bottom_height) if top_height > 0: - yield Rect(wg.left, wg.top - top_height, wg.right + right_width, wg.top) + yield Rect(left, top - top_height, right + right_width, top) if right_width > 0: - yield Rect(wg.right, wg.top, wg.right + right_width, wg.bottom + bottom_height) + yield Rect(right, top, right + right_width, bottom + bottom_height) if bottom_height > 0: - yield Rect(wg.left, wg.bottom, wg.right, wg.bottom + bottom_height) + yield Rect(left, bottom, right, bottom + bottom_height) -def window_geometry(xstart: int, xnum: int, ystart: int, ynum: int, left: int, top: int, right: int, bottom: int) -> WindowGeometry: +def window_geometry( + xstart: int, xnum: int, ystart: int, ynum: int, left: int, top: int, right: int, bottom: int, + compensatory: Edges = Edges(), +) -> WindowGeometry: return WindowGeometry( left=xstart, top=ystart, xnum=max(0, xnum), ynum=max(0, ynum), right=xstart + lgd.cell_width * xnum, bottom=ystart + lgd.cell_height * ynum, - spaces=Edges(left, top, right, bottom) + spaces=Edges(left, top, right, bottom), compensatory=compensatory, ) def window_geometry_from_layouts(x: LayoutData, y: LayoutData) -> WindowGeometry: - return window_geometry(x.content_pos, x.cells_per_window, y.content_pos, y.cells_per_window, x.space_before, y.space_before, x.space_after, y.space_after) + return window_geometry( + x.content_pos, x.cells_per_window, y.content_pos, y.cells_per_window, + x.space_before, y.space_before, x.space_after, y.space_after, + Edges(x.compensatory_before, y.compensatory_before, x.compensatory_after, y.compensatory_after), + ) def layout_single_window( diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 8dd811768..5da8e1b03 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -1741,6 +1741,25 @@ The value can be one of: :code:`top-left`, :code:`top`, :code:`top-right`, """, ) +opt( + 'padding_fill_strategy', + 'background', + choices=('background', 'neighboring_cell'), + ctype='padding_fill_strategy', + long_text=""" +When the window size is not an exact multiple of the cell size, thin strips of +compensatory padding are added at the window edges (see +:opt:`placement_strategy`). This option controls how those strips are colored. +:code:`neighboring_cell` colors each strip to match the +background color of the cell adjacent to it, which looks best with full screen +applications such as editors that have differently colored border cells. A value +of :code:`background` colors the strips using the window background +color. Note that this only affects the compensatory padding, the intentional +padding from :opt:`window_padding_width` is always drawn using the background +color. +""", +) + opt( 'active_border_color', '#00ff00', diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 6f99de673..8022e6d24 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -1189,6 +1189,14 @@ class Parser: def open_url_with(self, val: str, ans: dict[str, typing.Any]) -> None: ans['open_url_with'] = to_cmdline(val) + def padding_fill_strategy(self, val: str, ans: dict[str, typing.Any]) -> None: + val = val.lower() + if val not in self.choices_for_padding_fill_strategy: + raise ValueError(f"The value {val} is not a valid choice for padding_fill_strategy") + ans["padding_fill_strategy"] = val + + choices_for_padding_fill_strategy = frozenset(('background', 'neighboring_cell')) + def palette_generate(self, val: str, ans: dict[str, typing.Any]) -> None: val = val.lower() if val not in self.choices_for_palette_generate: diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index 5bb372e9e..156fb679c 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -876,6 +876,19 @@ convert_from_opts_linux_bell_theme(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_padding_fill_strategy(PyObject *val, Options *opts) { + opts->padding_fill_strategy = padding_fill_strategy(val); +} + +static void +convert_from_opts_padding_fill_strategy(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "padding_fill_strategy"); + if (ret == NULL) return; + convert_from_python_padding_fill_strategy(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_active_border_color(PyObject *val, Options *opts) { opts->active_border_color = active_border_color(val); @@ -1675,6 +1688,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_linux_bell_theme(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_padding_fill_strategy(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_active_border_color(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_inactive_border_color(py_opts, opts); diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index d4cc59046..a46d2b6b3 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -150,6 +150,12 @@ bglayout(PyObject *layout_name) { return TILING; } +static inline PaddingFillStrategy +padding_fill_strategy(PyObject *val) { + const char *name = PyUnicode_AsUTF8(val); + return name[0] == 'n' ? PADDING_FILL_NEIGHBORING_CELL : PADDING_FILL_BACKGROUND; +} + static inline ImageAnchorPosition bganchor(PyObject *anchor_name) { const char *name = PyUnicode_AsUTF8(anchor_name); diff --git a/kitty/options/types.py b/kitty/options/types.py index cf7009fb1..665760137 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -26,6 +26,7 @@ choices_for_focus_follows_mouse = typing.Literal['no', 'n', 'false', 'y', 'yes', choices_for_linux_display_server = typing.Literal['auto', 'wayland', 'x11'] choices_for_macos_colorspace = typing.Literal['srgb', 'default', 'displayp3'] choices_for_macos_show_window_title_in = typing.Literal['all', 'menubar', 'none', 'window'] +choices_for_padding_fill_strategy = typing.Literal['background', 'neighboring_cell'] choices_for_palette_generate = typing.Literal['fixed', 'semantic', 'legacy'] choices_for_placement_strategy = typing.Literal['top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right'] choices_for_pointer_shape_when_grabbed = choices_for_default_pointer_shape @@ -416,6 +417,7 @@ option_names = ( 'narrow_symbols', 'notify_on_cmd_finish', 'open_url_with', + 'padding_fill_strategy', 'palette_generate', 'paste_actions', 'pixel_scroll', @@ -627,6 +629,7 @@ class Options: mouse_hide_wait: MouseHideWait = MouseHideWait(hide_wait=0.0, show_wait=0.0, show_threshold=40, scroll_show=True) if is_macos else MouseHideWait(hide_wait=3.0, show_wait=0.0, show_threshold=40, scroll_show=True) notify_on_cmd_finish: NotifyOnCmdFinish = NotifyOnCmdFinish(when='never', duration=5.0, action='notify', cmdline=(), clear_on=('focus', 'next')) open_url_with: list[str] = ['default'] + padding_fill_strategy: choices_for_padding_fill_strategy = 'background' palette_generate: choices_for_palette_generate = 'fixed' paste_actions: frozenset[str] = frozenset({'confirm', 'quote-urls-at-prompt'}) pixel_scroll: bool = True diff --git a/kitty/shaders.c b/kitty/shaders.c index 8ed65c5ac..a5ebb50e4 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -26,6 +26,7 @@ enum { BLIT_PROGRAM, SCREENSHOT_PROGRAM, ROUNDED_RECT_PROGRAM, + PADDING_PROGRAM, NUM_PROGRAMS }; enum { SPRITE_MAP_UNIT, GRAPHICS_UNIT, SPRITE_DECORATIONS_MAP_UNIT }; @@ -370,6 +371,20 @@ init_cell_program(void) { UniformBlock border_glut = program_uniform_block(BORDERS_PROGRAM, "GammaLUT"); glUniformBlockBinding(program_id(BORDERS_PROGRAM), border_glut.index, GAMMA_LUT_BINDING_POINT); + // The padding program shares the cell background computation and so uses the + // same uniform blocks bound to the same binding points as the cell programs. + { + UniformBlock crd = program_uniform_block(PADDING_PROGRAM, "CellRenderData"); + glUniformBlockBinding(program_id(PADDING_PROGRAM), crd.index, CELL_RENDER_DATA_BINDING_POINT); + UniformBlock ct = program_uniform_block(PADDING_PROGRAM, "ColorTable"); + glUniformBlockBinding(program_id(PADDING_PROGRAM), ct.index, COLOR_TABLE_BINDING_POINT); + UniformBlock glut = program_uniform_block(PADDING_PROGRAM, "GammaLUT"); + glUniformBlockBinding(program_id(PADDING_PROGRAM), glut.index, GAMMA_LUT_BINDING_POINT); + } +#define C(name, expected) { int aloc = attrib_location(PADDING_PROGRAM, #name); if (aloc != expected && aloc != -1) fatal("The attribute location for %s is %d != %d in the padding program", #name, aloc, expected); } + C(colors, 0); C(sprite_idx, 1); C(is_selected, 2); +#undef C + // The gamma LUT is a constant, shared amongst all the programs that use it via a single UBO. if (shader_globals_vao_idx == -1) { shader_globals_vao_idx = create_vao(); @@ -1331,6 +1346,98 @@ draw_cells_without_layers(const UIRenderData *ui, ssize_t vao_idx) { call_cell_program(CELL_PROGRAM, ui, vao_idx, true, DRAW_BOTH_BG); } +static void +configure_cell_vao_attributes(ssize_t vao_idx, unsigned int base_cell, unsigned int cell_step) { + // (Re)point the instanced cell attributes so that instance i corresponds to + // the cell at (base_cell + i*cell_step). Used to draw a single padding strip + // from the shared cell VAO, and to restore the canonical layout afterwards + // (base_cell=0, cell_step=1). The VAO must be bound before calling this. + CELL_BUFFERS; + const GLsizei cell_stride = (GLsizei)(cell_step * sizeof(GPUCell)); + const uintptr_t cell_base = (uintptr_t)base_cell * sizeof(GPUCell); + set_vao_attribute(vao_idx, cell_data_buffer, program_attribute_location(CELL_PROGRAM, "sprite_idx"), + 2, GL_UNSIGNED_INT, cell_stride, (void*)(cell_base + offsetof(GPUCell, sprite_idx)), 1); + set_vao_attribute(vao_idx, cell_data_buffer, program_attribute_location(CELL_PROGRAM, "colors"), + 3, GL_UNSIGNED_INT, cell_stride, (void*)(cell_base + offsetof(GPUCell, fg)), 1); + set_vao_attribute(vao_idx, selection_buffer, program_attribute_location(CELL_PROGRAM, "is_selected"), + 1, GL_UNSIGNED_BYTE, (GLsizei)(cell_step * sizeof(GLubyte)), (void*)((uintptr_t)base_cell * sizeof(GLubyte)), 1); +} + +static void +draw_padding_strip( + ssize_t vao_idx, bool for_final_output, unsigned int is_horizontal, unsigned int count, + unsigned int base_cell, unsigned int cell_step, float across0, float across1, + float along_start, float along_step, float clamp_lo, float clamp_hi +) { + if (!count) return; + configure_cell_vao_attributes(vao_idx, base_cell, cell_step); +#define L(x) program_uniform_location(PADDING_PROGRAM, #x) + glUniform1ui(L(is_horizontal), is_horizontal); + glUniform1ui(L(along_count), count); + glUniform1ui(L(base_instance), base_cell); + glUniform1ui(L(instance_step), cell_step); + glUniform2f(L(across), across0, across1); + glUniform1f(L(along_start), along_start); + glUniform1f(L(along_step), along_step); + glUniform2f(L(along_clamp), clamp_lo, clamp_hi); +#undef L + draw_quad(!for_final_output, count); +} + +static void +draw_window_padding(const UIRenderData *ui, Window *window, ssize_t vao_idx, bool for_final_output) { + // Color the compensatory padding strips (the innermost slice of the window + // padding, arising from the window size not being an exact multiple of the + // cell size) to match their neighboring cell. The strips lie outside the + // per-window cell viewport, so this runs with the full framebuffer viewport. + if (!window || OPT(padding_fill_strategy) != PADDING_FILL_NEIGHBORING_CELL) return; + const unsigned int cl = window->size_mismatch_padding.left, ct = window->size_mismatch_padding.top, + cr = window->size_mismatch_padding.right, cb = window->size_mismatch_padding.bottom; + if (!(cl | ct | cr | cb)) return; + Screen *screen = ui->screen; + const unsigned int columns = screen->columns, lines = screen->lines; + if (!columns || !lines) return; + const unsigned int render_offset = pixel_scroll_enabled(screen) ? 1u : 0u; + const unsigned int top_row = render_offset, bottom_row = render_offset + lines - 1u; + + const float fbw = (float)ui->full_framebuffer_width, fbh = (float)ui->full_framebuffer_height; + const float cw = (float)ui->cell_width, ch = (float)ui->cell_height; + const float L = (float)ui->screen_left, T = (float)ui->screen_top; + const float R = L + (float)ui->screen_width, B = T + (float)ui->screen_height; +#define NX(px) (2.f * (px) / fbw - 1.f) +#define NY(px) (1.f - 2.f * (px) / fbh) + const float dx = 2.f * cw / fbw, dy = 2.f * ch / fbh; + + bind_program(PADDING_PROGRAM); + bind_vertex_array(vao_idx); + CELL_BUFFERS; + bind_vao_uniform_buffer(vao_idx, uniform_buffer, CELL_RENDER_DATA_BINDING_POINT); + bind_vao_uniform_buffer(vao_idx, color_table_buffer, COLOR_TABLE_BINDING_POINT); + if (for_final_output) glEnable(GL_FRAMEBUFFER_SRGB); + + // Top strip: spans content width, per top-row cell. across selected by cell + // corner: top corner -> outer edge (T-ct), bottom corner -> content edge (T). + if (ct) draw_padding_strip(vao_idx, for_final_output, 1u, columns, top_row * columns, 1u, + NY(T - ct), NY(T), NX(L), dx, NX(L), NX(R)); + // Bottom strip: top corner -> content edge (B), bottom corner -> outer (B+cb). + if (cb) draw_padding_strip(vao_idx, for_final_output, 1u, columns, bottom_row * columns, 1u, + NY(B), NY(B + cb), NX(L), dx, NX(L), NX(R)); + // Left strip: full comp-frame height (corners via along_clamp), per left-column + // cell. left corner -> outer (L-cl), right corner -> content edge (L). + if (cl) draw_padding_strip(vao_idx, for_final_output, 0u, lines, top_row * columns, columns, + NX(L - cl), NX(L), NY(T), -dy, NY(T - ct), NY(B + cb)); + // Right strip: left corner -> content edge (R), right corner -> outer (R+cr). + if (cr) draw_padding_strip(vao_idx, for_final_output, 0u, lines, top_row * columns + (columns - 1u), columns, + NX(R), NX(R + cr), NY(T), -dy, NY(T - ct), NY(B + cb)); + + if (for_final_output) glDisable(GL_FRAMEBUFFER_SRGB); + // Restore the canonical cell attribute layout so cell rendering is unaffected. + configure_cell_vao_attributes(vao_idx, 0u, 1u); + unbind_program(); +#undef NX +#undef NY +} + static void draw_tint(const UIRenderData *ui) { bind_program(TINT_PROGRAM); @@ -1433,6 +1540,9 @@ draw_cells(const WindowRenderData *srd, OSWindow *os_window, bool is_active_wind if (ui.os_window->needs_layers) draw_cells_with_layers(&ui, srd->vao_idx); else draw_cells_without_layers(&ui, srd->vao_idx); restore_viewport(); + // The compensatory padding lies outside the per-window cell viewport, so it + // is drawn after restoring the full framebuffer viewport. + draw_window_padding(&ui, window, srd->vao_idx, !ui.os_window->needs_layers); } // }}} @@ -1905,6 +2015,7 @@ init_shaders(PyObject *module) { C(CELL_PROGRAM); C(CELL_FG_PROGRAM); C(CELL_BG_PROGRAM); C(BORDERS_PROGRAM); C(GRAPHICS_PROGRAM); C(GRAPHICS_PREMULT_PROGRAM); C(GRAPHICS_ALPHA_MASK_PROGRAM); C(BGIMAGE_PROGRAM); C(TINT_PROGRAM); C(TRAIL_PROGRAM); C(BLIT_PROGRAM); C(SCREENSHOT_PROGRAM); C(ROUNDED_RECT_PROGRAM); + C(PADDING_PROGRAM); C(GLSL_VERSION); C(GL_VERSION); C(GL_VENDOR); diff --git a/kitty/shaders/padding.slang b/kitty/shaders/padding.slang new file mode 100644 index 000000000..889f711fd --- /dev/null +++ b/kitty/shaders/padding.slang @@ -0,0 +1,99 @@ +#language slang 2026 +// Copyright (C) 2026 Kovid Goyal +// Distributed under terms of the GPLv3 license. + +// https://github.com/shader-slang/slang/issues/11874 +// warnings-disable: 41012 + +import utils; +import background; + +// This shader colors the compensatory window padding (the thin strips that +// appear when the window size is not an exact multiple of the cell size) so +// that each strip matches the background of its neighboring cell. It re-uses +// the cell VAO (colors/sprite_idx/is_selected instanced attributes) and the +// background module's padding_background_premul() to compute the color. +// +// It is invoked once per edge (a "strip") with the cell VAO attributes +// re-pointed so that instance i corresponds to the i'th cell along that strip. +// The geometry of the quad is computed entirely from uniforms in +// framebuffer-NDC, since the padding lies outside the per-window cell viewport +// and is therefore drawn with the full framebuffer viewport. + +// Corner indicator per quad vertex, matching cell_pos_map in background.slang. +// .x: 0 -> left, 1 -> right ; .y: 0 -> top, 1 -> bottom +static const uint2 pad_pos_map[4] = { + uint2(1u, 0u), // right, top + uint2(1u, 1u), // right, bottom + uint2(0u, 1u), // left, bottom + uint2(0u, 0u) // left, top +}; + +struct VertexOutput { + float4 color_premul : COLOR_PREMUL; + float4 position : SV_Position; +}; + +[shader("vertex")] +VertexOutput vertex_main( + [[vk::location(0)]] uint3 colors, + [[vk::location(1)]] uint2 sprite_idx, + [[vk::location(2)]] uint is_selected, + uint vertex_id : SV_VertexID, + uint instance_id : SV_InstanceID, + // The true cell index (into the cell grid) of this strip cell, used only to + // reconstruct the row/column for the shared background computation. It is + // base_instance + instance_id * instance_step and matches the offset/stride + // used to re-point the VAO attributes on the C side. + uniform uint base_instance, + uniform uint instance_step, + // 1 for the top/bottom strips (the strip runs along x), 0 for left/right. + uniform uint is_horizontal, + // Number of cells along the strip (== instance count). + uniform uint along_count, + // The two NDC values of the thin (across) dimension of the strip, selected + // by the quad corner: .x for the near-content corner mapping, .y for the + // other. The C side assigns these per edge. + uniform float2 across, + // NDC of the along dimension for the first cell's near corner, and the step + // per cell (signed; negative for the vertical strips as NDC y decreases + // downwards). + uniform float along_start, + uniform float along_step, + // NDC bounds used to extend the first/last cell so the strip fills the + // corners (a no-op when set to the natural strip extent). + uniform float2 along_clamp, +) { + VertexOutput vo; + uint real_id = base_instance + instance_id * instance_step; + vo.color_premul = padding_background_premul(colors, sprite_idx, is_selected, real_id); + + uint2 p = pad_pos_map[vertex_id]; + float along_lo = along_start + float(instance_id) * along_step; + float along_hi = along_lo + along_step; + + // Branch-free selection via lerp(). h selects the strip orientation, s is the + // corner component along the strip axis and a the component across it. + float h = float(is_horizontal); + float s = lerp(float(p.y), float(p.x), h); + float a = lerp(float(p.x), float(p.y), h); + // 1 when this is the first/last cell along the strip, else 0. + float is_first = 1.0 - min(float(instance_id), 1.0); + float is_last = 1.0 - min(float(along_count - 1u - instance_id), 1.0); + + float along = lerp(along_lo, along_hi, s); + // Extend the first cell's near corner and the last cell's far corner out to + // along_clamp so the strip fills the corners. + along = lerp(along, along_clamp.x, is_first * (1.0 - s)); + along = lerp(along, along_clamp.y, is_last * s); + float across_v = lerp(across.x, across.y, a); + + // Horizontal strip -> position is (along, across); vertical -> (across, along). + vo.position = float4(lerp(across_v, along, h), lerp(along, across_v, h), 0.0, 1.0); + return vo; +} + +[shader("fragment")] +float4 fragment_main(float4 color_premul : COLOR_PREMUL) : SV_Target { + return color_premul; +} diff --git a/kitty/shaders/slang.py b/kitty/shaders/slang.py index 1137eb63d..f47a20f0b 100644 --- a/kitty/shaders/slang.py +++ b/kitty/shaders/slang.py @@ -41,6 +41,7 @@ from kitty.fast_data_types import ( GRAPHICS_PROGRAM, MARK, MARK_MASK, + PADDING_PROGRAM, REVERSE, ROUNDED_RECT_PROGRAM, SCREENSHOT_PROGRAM, @@ -211,6 +212,7 @@ class LoadShaderPrograms: 'screenshot': SCREENSHOT_PROGRAM, 'rounded_rect': ROUNDED_RECT_PROGRAM, 'border': BORDERS_PROGRAM, + 'padding': PADDING_PROGRAM, }.items(): vert, frag = glsl_shaders(name) compile_program(prog, (vert,), (frag,), metadata[name], allow_recompile) diff --git a/kitty/state.c b/kitty/state.c index 837a154ff..38b199f09 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -1183,12 +1183,16 @@ PYWRAP1(set_window_render_data) { id_type os_window_id, tab_id, window_id; WindowGeometry g = {0}; Screen *screen; - PA("KKKOIIIIIIII", &os_window_id, &tab_id, &window_id, &screen, + unsigned int cl, ct, cr, cb; + PA("KKKOIIIIIIIIIIII", &os_window_id, &tab_id, &window_id, &screen, B(left), B(top), B(right), B(bottom), - S(left), S(top), S(right), S(bottom)); + S(left), S(top), S(right), S(bottom), + &cl, &ct, &cr, &cb); WITH_WINDOW(os_window_id, tab_id, window_id); init_window_render_data(&window->render_data, g, screen); + window->size_mismatch_padding.left = cl; window->size_mismatch_padding.top = ct; + window->size_mismatch_padding.right = cr; window->size_mismatch_padding.bottom = cb; END_WITH_WINDOW; Py_RETURN_NONE; #undef B diff --git a/kitty/state.h b/kitty/state.h index 50149872d..90f57991e 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -103,6 +103,7 @@ typedef struct Options { unsigned generation; } background_images; BackgroundImageLayout background_image_layout; + PaddingFillStrategy padding_fill_strategy; ImageAnchorPosition window_logo_position; bool background_image_linear; float background_tint, background_tint_gaps, window_logo_alpha; @@ -271,6 +272,14 @@ typedef struct Window { struct { unsigned int left, top, right, bottom; } padding; + // Compensatory padding arising from the window size not being an exact + // multiple of the cell size. This is the innermost slice of the padding, + // adjacent to the cells, and (when padding_fill_strategy is + // neighboring_cell) is colored to match the neighboring cell by the padding + // shader rather than being drawn in the background color. + struct { + unsigned int left, top, right, bottom; + } size_mismatch_padding; ClickQueue click_queues[8]; monotonic_t last_drag_scroll_at; uint32_t last_special_key_pressed; diff --git a/kitty/types.py b/kitty/types.py index 994960e38..0d24819dd 100644 --- a/kitty/types.py +++ b/kitty/types.py @@ -54,6 +54,11 @@ class WindowGeometry(NamedTuple): xnum: int ynum: int spaces: Edges = Edges() + # The part of spaces that comes from the window size not being an exact + # multiple of the cell size (as opposed to intentional margin/border/padding + # decoration). This is the innermost slice of the padding, adjacent to the + # cells, see layout_dimension(). + compensatory: Edges = Edges() class SignalInfo(NamedTuple): diff --git a/kitty/window.py b/kitty/window.py index 0f447f080..802b85cb4 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -1060,7 +1060,8 @@ class Window: # Set C-side render data with adjusted top/bottom for content area set_window_render_data(self.os_window_id, self.tab_id, self.id, self.screen, g.left, render_top, g.right, render_bottom, - g.spaces.left, g.spaces.top, g.spaces.right, g.spaces.bottom) + g.spaces.left, g.spaces.top, g.spaces.right, g.spaces.bottom, + g.compensatory.left, g.compensatory.top, g.compensatory.right, g.compensatory.bottom) self.update_effective_padding() # Handle title bar screen @@ -1930,9 +1931,6 @@ class Window: def destroy(self) -> None: self.call_watchers(self.watchers.on_close, {}) self.destroyed = True - if self.clear_progress_timer: - remove_timer(self.clear_progress_timer) - self.clear_progress_timer = 0 self.clipboard_request_manager.close() del self.kitten_result_processors if hasattr(self, 'screen'): diff --git a/local-agent.md b/local-agent.md index 26282066e..00ef6b7ab 100644 --- a/local-agent.md +++ b/local-agent.md @@ -29,7 +29,12 @@ make debug Execute the following two commands to fix any formatting issues in your code: ``` ruff check --fix -gofmt -s -l -w tools kittens +git ls-files '*.go' | xargs gofmt -w -s -l +``` + +Run the following command to type check python files: +``` +./test.py type-check ``` ### 🧪 Test Commands