diff --git a/kitty/bgimage_fragment.glsl b/kitty/bgimage_fragment.glsl new file mode 100644 index 000000000..d8a6d4c14 --- /dev/null +++ b/kitty/bgimage_fragment.glsl @@ -0,0 +1,26 @@ +#version GLSL_VERSION +#define LAYOUT_TYPE + +uniform sampler2D image; +uniform float bgimage_opacity; +#ifdef TILED +uniform float bgimage_scale; + +// These are of the window, not the screen. +uniform float width; +uniform float height; +#endif +in vec2 texcoord; +out vec4 color; + +void main() { +#ifdef TILED + vec2 txsz = vec2(width,height) / textureSize(image,0); + txsz /= bgimage_scale; + color = texture(image, texcoord * txsz); +#endif +#ifdef SIMPLE + color = texture(image, texcoord); +#endif + color = vec4(color.rgb, color.a * bgimage_opacity); +} diff --git a/kitty/bgimage_vertex.glsl b/kitty/bgimage_vertex.glsl new file mode 100644 index 000000000..a53d8b320 --- /dev/null +++ b/kitty/bgimage_vertex.glsl @@ -0,0 +1,9 @@ +#version GLSL_VERSION + +layout(location=0) in vec4 src; +out vec2 texcoord; + +void main() { + texcoord = clamp(vec2(src[0], src[1]*-1), 0, 1); + gl_Position = src; +} diff --git a/kitty/borders.py b/kitty/borders.py index 22c750a5b..0e609d471 100644 --- a/kitty/borders.py +++ b/kitty/borders.py @@ -60,11 +60,13 @@ class Borders: active_window, current_layout, extra_blank_rects, - draw_window_borders=True + draw_window_borders=True, + draw_blank_rects=False ): add_borders_rect(self.os_window_id, self.tab_id, 0, 0, 0, 0, BorderColor.default_bg) - for br in chain(current_layout.blank_rects, extra_blank_rects): - add_borders_rect(self.os_window_id, self.tab_id, *br, BorderColor.default_bg) + if draw_blank_rects: + for br in chain(current_layout.blank_rects, extra_blank_rects): + add_borders_rect(self.os_window_id, self.tab_id, *br, BorderColor.default_bg) bw, pw = self.border_width, self.padding_width if bw + pw <= 0: return diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index a79df5356..055506d1c 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -657,6 +657,7 @@ render(monotonic_t now, bool input_read) { for (size_t i = 0; i < global_state.num_os_windows; i++) { OSWindow *w = global_state.os_windows + i; + w->render_calls++; if (!w->num_tabs) continue; if (!should_os_window_be_rendered(w)) { update_os_window_title(w); @@ -688,6 +689,7 @@ render(monotonic_t now, bool input_read) { if (!w->fonts_data) { log_error("No fonts data found for window id: %llu", w->id); continue; } if (prepare_to_render_os_window(w, now, &active_window_id, &active_window_bg, &num_visible_windows, &all_windows_have_same_bg)) needs_render = true; if (w->last_active_window_id != active_window_id || w->last_active_tab != w->active_tab || w->focused_at_last_render != w->is_focused) needs_render = true; + if (w->render_calls < 3 && w->bgimage != NULL) needs_render = true; if (needs_render) render_os_window(w, now, active_window_id, active_window_bg, num_visible_windows, all_windows_have_same_bg); } last_render_at = now; diff --git a/kitty/config_data.py b/kitty/config_data.py index 3e611d490..9a90b5b1e 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -846,6 +846,37 @@ of windows set :opt:`dynamic_background_opacity` to :code:`yes` (this is off by default as it has a performance cost) ''')) + +def startup_session(x): + if x.lower() == 'none': + return + x = os.path.expanduser(x) + x = os.path.expandvars(x) + if not os.path.isabs(x): + x = os.path.join(config_dir, x) + return x + + +o('background_image', 'none', option_type=startup_session, long_text=_(''' +Path to a background image. Must be PNG.''')) + +o('background_image_layout', 'tiling', option_type=choices('tiling', 'scaled', 'mirror_tiled'), long_text=_(''' +Whether to tile or scale the background image.''')) + +o('background_image_linear', False, long_text=_(''' +When background image is scaled, whether linear interpolation should be used.''')) + +o('background_image_opacity', 0.5, option_type=positive_float, long_text=_(''' +Background image opacity, between 0.0 and 1.0 inclusive. This +can only ever decrease a background's opacity, if the image is already +semi-transparent, "1" is interpreted as the image's current transparency.''')) + +o('background_image_scale', 1.0, option_type=positive_float, long_text=_(''' +Only has an effect if :opt:`background_image_layout` is tiling, should be positive.''')) + +o('background_image_scale', 1.0, option_type=positive_float, long_text=_(''' +Only has an effect if :opt:`background_image_layout` is tiling, should be positive.''')) + o('dynamic_background_opacity', False, long_text=_(''' Allow changing of the :opt:`background_opacity` dynamically, using either keyboard shortcuts (:sc:`increase_background_opacity` and :sc:`decrease_background_opacity`) @@ -966,16 +997,6 @@ The default is to check every 24 hrs, set to zero to disable. ''')) -def startup_session(x): - if x.lower() == 'none': - return - x = os.path.expanduser(x) - x = os.path.expandvars(x) - if not os.path.isabs(x): - x = os.path.join(config_dir, x) - return x - - o('startup_session', 'none', option_type=startup_session, long_text=_(''' Path to a session file to use for all kitty instances. Can be overridden by using the :option:`kitty --session` command line option for individual diff --git a/kitty/data-types.h b/kitty/data-types.h index aabe751d0..edba314fa 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -55,6 +55,7 @@ typedef enum MouseTrackingModes { NO_TRACKING, BUTTON_MODE, MOTION_MODE, ANY_MOD typedef enum MouseTrackingProtocols { NORMAL_PROTOCOL, UTF8_PROTOCOL, SGR_PROTOCOL, URXVT_PROTOCOL} MouseTrackingProtocol; typedef enum MouseShapes { BEAM, HAND, ARROW } MouseShape; typedef enum { NONE, MENUBAR, WINDOW, ALL } WindowTitleIn; +typedef enum { TILING, SCALED, MIRRORED } BackgroundImageLayout; #define MAX_CHILDREN 512 #define BLANK_CHAR 0 diff --git a/kitty/glfw.c b/kitty/glfw.c index 79ac0460b..1364bcf4a 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -377,7 +377,7 @@ make_os_window_context_current(OSWindow *w) { } -static inline void +void get_window_content_scale(GLFWwindow *w, float *xscale, float *yscale, double *xdpi, double *ydpi) { if (w) glfwGetWindowContentScale(w, xscale, yscale); else { diff --git a/kitty/graphics.c b/kitty/graphics.c index b1d49515b..a0789e384 100644 --- a/kitty/graphics.c +++ b/kitty/graphics.c @@ -5,6 +5,7 @@ * Distributed under terms of the GPL3 license. */ +#include "gl.h" #include "graphics.h" #include "state.h" @@ -29,9 +30,8 @@ static bool send_to_gpu = true; GraphicsManager* grman_alloc() { GraphicsManager *self = (GraphicsManager *)GraphicsManager_Type.tp_alloc(&GraphicsManager_Type, 0); - self->images_capacity = 64; + self->images_capacity = self->capacity = 64; self->images = calloc(self->images_capacity, sizeof(Image)); - self->capacity = 64; self->render_data = calloc(self->capacity, sizeof(ImageRenderData)); if (self->images == NULL || self->render_data == NULL) { PyErr_NoMemory(); @@ -248,6 +248,38 @@ add_trim_predicate(Image *img) { return !img->data_loaded || (!img->client_id && !img->refcnt); } +bool png_path_to_bitmap(uint8_t** data, unsigned int* width, unsigned int* height, size_t* sz) { + const char* path = OPT(background_image); + if (access(path, R_OK) != 0) { + log_error("File %s, (requested background image,) does not exist (%d)", path, errno); + return false; + } + FILE* fp = fopen(path, "r"); + if (fp == NULL) { + log_error("File %s, (requested background image,) could not be opened", path); + return false; + } + fseek(fp, 0L, SEEK_END); + size_t filesize = ftell(fp); + *data = calloc(filesize, sizeof(char)); + fseek(fp, 0L, SEEK_SET); // rewind() deprecated on some platforms + fread(*data, sizeof(char), filesize, fp); + fclose(fp); + png_read_data d; + memset(&d, 0, sizeof(png_read_data)); + inflate_png_inner(&d, *data, filesize); + if (!d.ok) { + log_error("File %s, (requested background image,) not readable by libpng", path); + return false; + } + free(*data); + *data = d.decompressed; + *sz = d.sz; + *height = d.height; *width = d.width; + return true; +} + + static inline Image* find_or_create_image(GraphicsManager *self, uint32_t id, bool *existing) { if (id) { @@ -421,7 +453,7 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_ size_t required_sz = (img->load_data.is_opaque ? 3 : 4) * img->width * img->height; if (img->load_data.data_sz != required_sz) ABRT(EINVAL, "Image dimensions: %ux%u do not match data size: %zu, expected size: %zu", img->width, img->height, img->load_data.data_sz, required_sz); if (LIKELY(img->data_loaded && send_to_gpu)) { - send_image_to_gpu(&img->texture_id, img->load_data.data, img->width, img->height, img->load_data.is_opaque, img->load_data.is_4byte_aligned); + send_image_to_gpu(&img->texture_id, img->load_data.data, img->width, img->height, img->load_data.is_opaque, img->load_data.is_4byte_aligned, false, REPEAT_CLAMP); free_load_data(&img->load_data); self->used_storage += required_sz; img->used_storage = required_sz; diff --git a/kitty/graphics.h b/kitty/graphics.h index 16af6923c..bc0b8abca 100644 --- a/kitty/graphics.h +++ b/kitty/graphics.h @@ -55,6 +55,12 @@ typedef struct { size_t used_storage; } Image; +typedef struct { + uint32_t texture_id; + unsigned int height, width; + uint8_t* bitmap; +} BackgroundImage; + typedef struct { float vertices[16]; uint32_t texture_id, group_count; @@ -71,6 +77,8 @@ typedef struct { Image *images; size_t count, capacity; ImageRenderData *render_data; + Image bgimage; + ImageRenderData bgimage_rd; bool layers_dirty; // The number of images below MIN_ZINDEX / 2, then the number of refs between MIN_ZINDEX / 2 and -1 inclusive, then the number of refs above 0 inclusive. size_t num_of_below_refs, num_of_negative_refs, num_of_positive_refs; @@ -93,3 +101,4 @@ void grman_scroll_images(GraphicsManager *self, const ScrollData*, CellPixelSize void grman_resize(GraphicsManager*, index_type, index_type, index_type, index_type); void grman_rescale(GraphicsManager *self, CellPixelSize fg); void gpu_data_for_centered_image(ImageRenderData *ans, unsigned int screen_width_px, unsigned int screen_height_px, unsigned int width, unsigned int height); +bool png_path_to_bitmap(uint8_t** data, unsigned int* width, unsigned int* height, size_t* sz); diff --git a/kitty/screen.c b/kitty/screen.c index dac33f41e..58ebe6114 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -111,6 +111,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { self->historybuf = alloc_historybuf(MAX(scrollback, lines), columns, OPT(scrollback_pager_history_size)); self->main_grman = grman_alloc(); self->alt_grman = grman_alloc(); + self->grman = self->main_grman; self->pending_mode.wait_time = 2.0; self->disable_ligatures = OPT(disable_ligatures); diff --git a/kitty/shaders.c b/kitty/shaders.c index 83a0c8c42..17d9a8439 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -7,10 +7,12 @@ #include "fonts.h" #include "gl.h" +#include "png-reader.h" +#include #include -enum { CELL_PROGRAM, CELL_BG_PROGRAM, CELL_SPECIAL_PROGRAM, CELL_FG_PROGRAM, BORDERS_PROGRAM, GRAPHICS_PROGRAM, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_ALPHA_MASK_PROGRAM, BLIT_PROGRAM, NUM_PROGRAMS }; -enum { SPRITE_MAP_UNIT, GRAPHICS_UNIT, BLIT_UNIT }; +enum { CELL_PROGRAM, CELL_BG_PROGRAM, CELL_SPECIAL_PROGRAM, CELL_FG_PROGRAM, BORDERS_PROGRAM, GRAPHICS_PROGRAM, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_ALPHA_MASK_PROGRAM, BLIT_PROGRAM, BGIMAGE_PROGRAM, BGIMAGE_TILED_PROGRAM, NUM_PROGRAMS }; +enum { SPRITE_MAP_UNIT, GRAPHICS_UNIT, BLIT_UNIT, BGIMAGE_UNIT }; // Sprites {{{ typedef struct { @@ -22,6 +24,7 @@ typedef struct { static const SpriteMap NEW_SPRITE_MAP = { .xnum = 1, .ynum = 1, .last_num_of_layers = 1, .last_ynum = -1 }; static GLint max_texture_size = 0, max_array_texture_layers = 0; +//static uint8_t *bg_image = NULL; SPRITE_MAP_HANDLE alloc_sprite_map(unsigned int cell_width, unsigned int cell_height) { @@ -132,14 +135,23 @@ send_sprite_to_gpu(FONTS_DATA_HANDLE fg, unsigned int x, unsigned int y, unsigne } void -send_image_to_gpu(GLuint *tex_id, const void* data, GLsizei width, GLsizei height, bool is_opaque, bool is_4byte_aligned) { +send_image_to_gpu(GLuint *tex_id, const void* data, GLsizei width, GLsizei height, bool is_opaque, bool is_4byte_aligned, bool linear, RepeatStrategy repeat) { if (!(*tex_id)) { glGenTextures(1, tex_id); } glBindTexture(GL_TEXTURE_2D, *tex_id); glPixelStorei(GL_UNPACK_ALIGNMENT, is_4byte_aligned ? 4 : 1); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST); + RepeatStrategy r; + switch (repeat) { + case REPEAT_MIRROR: + r = GL_MIRRORED_REPEAT; break; + case REPEAT_CLAMP: + r = GL_CLAMP_TO_EDGE; break; + default: + r = GL_REPEAT; + } + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, r); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, r); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, is_opaque ? GL_RGB : GL_RGBA, GL_UNSIGNED_BYTE, data); } @@ -323,6 +335,47 @@ cell_prepare_to_render(ssize_t vao_idx, ssize_t gvao_idx, Screen *screen, GLfloa return changed; } +void +draw_bg(int program, OSWindow *w) { + if (w->bvao_idx == 0) { + const GLfloat screenrect[4][2] = { + { -1.0, 1.0 }, + { -1.0, -1.0 }, + { 1.0, -1.0 }, + { 1.0, 1.0 }, + }; + w->bvao_idx = create_vao(); + bind_vertex_array(w->bvao_idx); + glGenBuffers(1, &w->vbo_idx); + glBindBuffer(GL_ARRAY_BUFFER, w->vbo_idx); + glBufferData(GL_ARRAY_BUFFER, 4*2*sizeof(float), screenrect, GL_STATIC_DRAW); + } + bind_vertex_array(w->bvao_idx); + glBindBuffer(GL_ARRAY_BUFFER, w->vbo_idx); + + glClear(GL_COLOR_BUFFER_BIT); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0); + glEnableVertexAttribArray(0); + + bind_program(program); + static bool bgimage_constants_set; + if (!bgimage_constants_set) { + glUniform1i(glGetUniformLocation(program_id(program), "image"), BGIMAGE_UNIT); + glUniform1f(glGetUniformLocation(program_id(program), "bgimage_scale"), OPT(background_image_scale)); + glUniform1f(glGetUniformLocation(program_id(program), "bgimage_opacity"), OPT(background_image_opacity)); + bgimage_constants_set = true; + } + glUniform1f(glGetUniformLocation(program_id(program), "height"), (float)(w->window_height)); + glUniform1f(glGetUniformLocation(program_id(program), "width"), (float)(w->window_width)); + glActiveTexture(GL_TEXTURE0 + BGIMAGE_UNIT); + + glBindTexture(GL_TEXTURE_2D, w->bgimage->texture_id); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + + unbind_vertex_array(); + unbind_program(); +} + static void draw_graphics(int program, ssize_t vao_idx, ssize_t gvao_idx, ImageRenderData *data, GLuint start, GLuint count) { bind_program(program); @@ -389,16 +442,18 @@ draw_cells_simple(ssize_t vao_idx, ssize_t gvao_idx, Screen *screen) { } static void -draw_cells_interleaved(ssize_t vao_idx, ssize_t gvao_idx, Screen *screen) { +draw_cells_interleaved(ssize_t vao_idx, ssize_t gvao_idx, Screen *screen, OSWindow *w) { glEnable(GL_BLEND); BLEND_ONTO_OPAQUE; bind_program(CELL_BG_PROGRAM); // draw background for all cells glUniform1ui(cell_program_layouts[CELL_BG_PROGRAM].draw_bg_bitfield_location, 3); + // This gives users a way to still use background images without a compositor. + if (OPT(background_opacity) != 0.0) glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, screen->lines * screen->columns); - if (screen->grman->num_of_below_refs) { + if (screen->grman->num_of_below_refs || w->bgimage != NULL) { draw_graphics(GRAPHICS_PROGRAM, vao_idx, gvao_idx, screen->grman->render_data, 0, screen->grman->num_of_below_refs); bind_program(CELL_BG_PROGRAM); // draw background for non-default bg cells @@ -435,12 +490,13 @@ draw_cells_interleaved_premult(ssize_t vao_idx, ssize_t gvao_idx, Screen *screen glBindFramebuffer(GL_DRAW_FRAMEBUFFER, os_window->offscreen_framebuffer); glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, os_window->offscreen_texture_id, 0); /* if (glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) fatal("Offscreen framebuffer not complete"); */ + glEnable(GL_BLEND); + BLEND_PREMULT; + glClear(GL_COLOR_BUFFER_BIT); bind_program(CELL_BG_PROGRAM); // draw background for all cells glUniform1ui(cell_program_layouts[CELL_BG_PROGRAM].draw_bg_bitfield_location, 3); glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, screen->lines * screen->columns); - glEnable(GL_BLEND); - BLEND_PREMULT; if (screen->grman->num_of_below_refs) { draw_graphics(GRAPHICS_PREMULT_PROGRAM, vao_idx, gvao_idx, screen->grman->render_data, 0, screen->grman->num_of_below_refs); @@ -469,11 +525,10 @@ draw_cells_interleaved_premult(ssize_t vao_idx, ssize_t gvao_idx, Screen *screen if (screen->grman->num_of_positive_refs) draw_graphics(GRAPHICS_PREMULT_PROGRAM, vao_idx, gvao_idx, screen->grman->render_data, screen->grman->num_of_negative_refs + screen->grman->num_of_below_refs, screen->grman->num_of_positive_refs); - glDisable(GL_BLEND); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glEnable(GL_SCISSOR_TEST); // Now render the framebuffer to the screen - glEnable(GL_SCISSOR_TEST); bind_program(BLIT_PROGRAM); bind_vertex_array(blit_vertex_array); static bool blit_constants_set = false; if (!blit_constants_set) { @@ -484,6 +539,7 @@ draw_cells_interleaved_premult(ssize_t vao_idx, ssize_t gvao_idx, Screen *screen glBindTexture(GL_TEXTURE_2D, os_window->offscreen_texture_id); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); glDisable(GL_SCISSOR_TEST); + glDisable(GL_BLEND); } static inline void @@ -496,6 +552,7 @@ set_cell_uniforms(float current_inactive_text_alpha, bool force) { cell_uniform_data.amask_premult_loc = glGetUniformLocation(program_id(GRAPHICS_ALPHA_MASK_PROGRAM), "alpha_mask_premult"); #define S(prog, name, val, type) { bind_program(prog); glUniform##type(glGetUniformLocation(program_id(prog), #name), val); } S(GRAPHICS_PROGRAM, image, GRAPHICS_UNIT, 1i); + S(GRAPHICS_PROGRAM, image, BGIMAGE_UNIT, 1i); S(GRAPHICS_PREMULT_PROGRAM, image, GRAPHICS_UNIT, 1i); S(CELL_PROGRAM, sprites, SPRITE_MAP_UNIT, 1i); S(CELL_FG_PROGRAM, sprites, SPRITE_MAP_UNIT, 1i); S(CELL_PROGRAM, dim_opacity, OPT(dim_opacity), 1f); S(CELL_FG_PROGRAM, dim_opacity, OPT(dim_opacity), 1f); @@ -558,10 +615,10 @@ draw_cells(ssize_t vao_idx, ssize_t gvao_idx, GLfloat xstart, GLfloat ystart, GL ); #undef SCALE if (os_window->is_semi_transparent) { - if (screen->grman->count) draw_cells_interleaved_premult(vao_idx, gvao_idx, screen, os_window); + if (screen->grman->count || os_window->bgimage != NULL) draw_cells_interleaved_premult(vao_idx, gvao_idx, screen, os_window); else draw_cells_simple(vao_idx, gvao_idx, screen); } else { - if (screen->grman->num_of_negative_refs || screen->grman->num_of_below_refs) draw_cells_interleaved(vao_idx, gvao_idx, screen); + if (screen->grman->num_of_negative_refs || screen->grman->num_of_below_refs || os_window->bgimage != NULL) draw_cells_interleaved(vao_idx, gvao_idx, screen, os_window); else draw_cells_simple(vao_idx, gvao_idx, screen); } } @@ -604,16 +661,28 @@ create_border_vao(void) { void draw_borders(ssize_t vao_idx, unsigned int num_border_rects, BorderRect *rect_buf, bool rect_data_is_dirty, uint32_t viewport_width, uint32_t viewport_height, color_type active_window_bg, unsigned int num_visible_windows, bool all_windows_have_same_bg, OSWindow *w) { + glEnable(GL_BLEND); + BLEND_ONTO_OPAQUE; + + if (w->bgimage != NULL) { + int program; + if (OPT(background_image_layout) == TILING || OPT(background_image_layout) == MIRRORED) program = BGIMAGE_TILED_PROGRAM; + else program = BGIMAGE_PROGRAM; + + draw_bg(program, w); + } + if (num_border_rects) { + bind_vertex_array(vao_idx); + bind_program(BORDERS_PROGRAM); if (rect_data_is_dirty) { size_t sz = sizeof(GLuint) * 5 * num_border_rects; void *borders_buf_address = alloc_and_map_vao_buffer(vao_idx, sz, 0, GL_STATIC_DRAW, GL_WRITE_ONLY); if (borders_buf_address) memcpy(borders_buf_address, rect_buf, sz); unmap_vao_buffer(vao_idx, 0); } - bind_program(BORDERS_PROGRAM); #define CV3(x) (((float)((x >> 16) & 0xff))/255.f), (((float)((x >> 8) & 0xff))/255.f), (((float)(x & 0xff))/255.f) - glUniform1f(border_uniform_locations[BORDER_background_opacity], w->is_semi_transparent ? w->background_opacity : 1.0f); + glUniform1f(border_uniform_locations[BORDER_background_opacity], w->is_semi_transparent ? MAX(w->background_opacity, OPT(background_image_opacity)) : 1.0f); glUniform3f(border_uniform_locations[BORDER_active_border_color], CV3(OPT(active_border_color))); glUniform3f(border_uniform_locations[BORDER_inactive_border_color], CV3(OPT(inactive_border_color))); glUniform3f(border_uniform_locations[BORDER_bell_border_color], CV3(OPT(bell_border_color))); @@ -621,11 +690,11 @@ draw_borders(ssize_t vao_idx, unsigned int num_border_rects, BorderRect *rect_bu color_type default_bg = (num_visible_windows > 1 && !all_windows_have_same_bg) ? OPT(background) : active_window_bg; glUniform3f(border_uniform_locations[BORDER_default_bg], CV3(default_bg)); #undef CV3 - bind_vertex_array(vao_idx); glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, num_border_rects); - unbind_vertex_array(); + unbind_vertex_array(); unbind_program(); } + glDisable(GL_BLEND); } // }}} @@ -722,7 +791,7 @@ static PyMethodDef module_methods[] = { bool init_shaders(PyObject *module) { #define C(x) if (PyModule_AddIntConstant(module, #x, x) != 0) { PyErr_NoMemory(); return false; } - C(CELL_PROGRAM); C(CELL_BG_PROGRAM); C(CELL_SPECIAL_PROGRAM); C(CELL_FG_PROGRAM); C(BORDERS_PROGRAM); C(GRAPHICS_PROGRAM); C(GRAPHICS_PREMULT_PROGRAM); C(GRAPHICS_ALPHA_MASK_PROGRAM); C(BLIT_PROGRAM); + C(CELL_PROGRAM); C(CELL_BG_PROGRAM); C(CELL_SPECIAL_PROGRAM); C(CELL_FG_PROGRAM); C(BORDERS_PROGRAM); C(GRAPHICS_PROGRAM); C(GRAPHICS_PREMULT_PROGRAM); C(GRAPHICS_ALPHA_MASK_PROGRAM); C(BLIT_PROGRAM); C(BGIMAGE_PROGRAM); C(BGIMAGE_TILED_PROGRAM); C(GLSL_VERSION); C(GL_VERSION); C(GL_VENDOR); diff --git a/kitty/state.c b/kitty/state.c index df83e0ecf..3f1522624 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -81,6 +81,39 @@ add_os_window() { ans->tab_bar_render_data.vao_idx = create_cell_vao(); ans->gvao_idx = create_graphics_vao(); ans->background_opacity = OPT(background_opacity); + + bool wants_bg = OPT(background_image) != NULL; + if (wants_bg) { + bool has_bg; + has_bg = global_state.bgimage != NULL; + + if (!has_bg) { + BackgroundImage* bgimage = calloc(1, sizeof(BackgroundImage)); + size_t size; + + has_bg = png_path_to_bitmap(&bgimage->bitmap, &bgimage->width, &bgimage->height, &size); + if (has_bg) { + bgimage->texture_id = 0; + RepeatStrategy r; + switch (OPT(background_image_layout)) { + case TILING: + r = REPEAT_DEFAULT; break; + case SCALED: + r = REPEAT_CLAMP; break; + case MIRRORED: + r = REPEAT_MIRROR; + } + send_image_to_gpu(&bgimage->texture_id, bgimage->bitmap, bgimage->width, + bgimage->height, false, true, false, r); + ans->bgimage = bgimage; + global_state.bgimage = bgimage; + } + } else { + // Reusing already loaded bgimage + ans->bgimage = global_state.bgimage; + } + } + ans->font_sz_in_pts = global_state.font_sz_in_pts; END_WITH_OS_WINDOW_REFS return ans; @@ -416,6 +449,17 @@ window_title_in(PyObject *title_in) { return ALL; } +static BackgroundImageLayout bglayout(PyObject *layout_name) { + const char *name = PyUnicode_AsUTF8(layout_name); + switch(name[0]) { + case 't': return TILING; + case 'm': return MIRRORED; + case 's': return SCALED; + default: break; + } + return TILING; +} + static MouseShape pointer_shape(PyObject *shape_name) { const char *name = PyUnicode_AsUTF8(shape_name); @@ -484,6 +528,10 @@ PYWRAP1(set_options) { S(cursor_blink_interval, parse_s_double_to_monotonic_t); S(cursor_stop_blinking_after, parse_s_double_to_monotonic_t); S(background_opacity, PyFloat_AsFloat); + S(background_image_opacity, PyFloat_AsFloat); + S(background_image_scale, PyFloat_AsFloat); + S(background_image_layout, bglayout); + S(background_image, (char*)PyUnicode_AsUTF8); S(dim_opacity, PyFloat_AsFloat); S(dynamic_background_opacity, PyObject_IsTrue); S(inactive_text_alpha, PyFloat_AsFloat); diff --git a/kitty/state.h b/kitty/state.h index 5ea22b4ff..279dfb437 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -13,6 +13,7 @@ typedef enum { LEFT_EDGE, TOP_EDGE, RIGHT_EDGE, BOTTOM_EDGE } Edge; typedef enum { RESIZE_DRAW_STATIC, RESIZE_DRAW_SCALED, RESIZE_DRAW_BLANK, RESIZE_DRAW_SIZE } ResizeDrawStrategy; +typedef enum { REPEAT_MIRROR, REPEAT_CLAMP, REPEAT_DEFAULT } RepeatStrategy; typedef struct { monotonic_t visual_bell_duration, cursor_blink_interval, cursor_stop_blinking_after, mouse_hide_wait, click_interval; @@ -37,6 +38,12 @@ typedef struct { int adjust_line_height_px, adjust_column_width_px; float adjust_line_height_frac, adjust_column_width_frac; float background_opacity, dim_opacity; + + char* background_image; + BackgroundImageLayout background_image_layout; + float background_image_opacity; + float background_image_scale; + bool dynamic_background_opacity; float inactive_text_alpha; float window_padding_width; @@ -136,6 +143,7 @@ typedef struct { int viewport_width, viewport_height, window_width, window_height; double viewport_x_ratio, viewport_y_ratio; Tab *tabs; + BackgroundImage *bgimage; unsigned int active_tab, num_tabs, capacity, last_active_tab, last_num_tabs, last_active_window_id; bool focused_at_last_render, needs_render; ScreenRenderData tab_bar_render_data; @@ -158,8 +166,10 @@ typedef struct { id_type temp_font_group_id; enum RENDER_STATE render_state; monotonic_t last_render_frame_received_at; + uint64_t render_calls; id_type last_focused_counter; - ssize_t gvao_idx; + ssize_t gvao_idx, bvao_idx; + unsigned int vbo_idx; } OSWindow; @@ -168,6 +178,7 @@ typedef struct { id_type os_window_id_counter, tab_id_counter, window_id_counter; PyObject *boss; + BackgroundImage *bgimage; OSWindow *os_windows; size_t num_os_windows, capacity; OSWindow *callback_os_window; @@ -223,11 +234,12 @@ void draw_centered_alpha_mask(OSWindow *w, size_t screen_width, size_t screen_he void update_surface_size(int, int, uint32_t); void free_texture(uint32_t*); void free_framebuffer(uint32_t*); -void send_image_to_gpu(uint32_t*, const void*, int32_t, int32_t, bool, bool); +void send_image_to_gpu(uint32_t*, const void*, int32_t, int32_t, bool, bool, bool, RepeatStrategy); void send_sprite_to_gpu(FONTS_DATA_HANDLE fg, unsigned int, unsigned int, unsigned int, pixel*); void blank_canvas(float, color_type); void blank_os_window(OSWindow *); void set_titlebar_color(OSWindow *w, color_type color); +void get_window_content_scale(GLFWwindow *w, float *xscale, float *yscale, double *xdpi, double *ydpi); FONTS_DATA_HANDLE load_fonts_data(double, double, double); void send_prerendered_sprites_for_window(OSWindow *w); #ifdef __APPLE__ diff --git a/kitty/window.py b/kitty/window.py index e598f5bec..615502ea0 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -15,14 +15,14 @@ from .constants import ( ScreenGeometry, WindowGeometry, appname, get_boss, wakeup ) from .fast_data_types import ( - BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM, CELL_PROGRAM, - CELL_SPECIAL_PROGRAM, CSI, DCS, DECORATION, DIM, - GRAPHICS_ALPHA_MASK_PROGRAM, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_PROGRAM, - MARK, MARK_MASK, OSC, REVERSE, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, - STRIKETHROUGH, Screen, add_window, cell_size_for_window, compile_program, - get_clipboard_string, init_cell_program, set_clipboard_string, - set_titlebar_color, set_window_render_data, update_window_title, - update_window_visibility, viewport_for_window + BLIT_PROGRAM, BGIMAGE_PROGRAM, BGIMAGE_TILED_PROGRAM, CELL_BG_PROGRAM, + CELL_FG_PROGRAM, CELL_PROGRAM, CELL_SPECIAL_PROGRAM, CSI, DCS, DECORATION, + DIM, GRAPHICS_ALPHA_MASK_PROGRAM, GRAPHICS_PREMULT_PROGRAM, + GRAPHICS_PROGRAM, MARK, MARK_MASK, OSC, REVERSE, SCROLL_FULL, SCROLL_LINE, + SCROLL_PAGE, STRIKETHROUGH, Screen, add_window, cell_size_for_window, + compile_program, get_clipboard_string, init_cell_program, + set_clipboard_string, set_titlebar_color, set_window_render_data, + update_window_title, update_window_visibility, viewport_for_window ) from .keys import defines, extended_key_event, keyboard_mode_name from .rgb import to_color @@ -83,6 +83,7 @@ def load_shader_programs(semi_transparent=False): vv = vv.replace('#define USE_SELECTION_FG', '#define DONT_USE_SELECTION_FG') ff = ff.replace('#define USE_SELECTION_FG', '#define DONT_USE_SELECTION_FG') compile_program(p, vv, ff) + v, f = load_shaders('graphics') for which, p in { 'SIMPLE': GRAPHICS_PROGRAM, @@ -91,6 +92,15 @@ def load_shader_programs(semi_transparent=False): }.items(): ff = f.replace('ALPHA_TYPE', which) compile_program(p, v, ff) + + v, f = load_shaders('bgimage') + for which, p in { + 'SIMPLE': BGIMAGE_PROGRAM, + 'TILED': BGIMAGE_TILED_PROGRAM, + }.items(): + ff = f.replace('LAYOUT_TYPE', which) + compile_program(p, v, ff) + init_cell_program()