From e053d1f5667b853329eec083489eb2034bfd1e80 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 17 May 2018 15:09:41 +0530 Subject: [PATCH] Implement resizing of individual windows in a layout See the Layouts section in the README for details. Fixes #362 Note that it is only implemented for the Tall layout currently. Other layouts to follow. The implementation might also be refined in the future. --- README.asciidoc | 12 ++++++- kitty/boss.py | 19 +++++++++- kitty/glfw.c | 11 ++++++ kitty/keys.c | 14 ++++++++ kitty/kitty.conf | 2 ++ kitty/layout.py | 90 +++++++++++++++++++++++++++++++++++++++++++++--- kitty/mouse.c | 1 + kitty/state.c | 17 +++++++++ kitty/state.h | 4 +++ kitty/tabs.py | 15 ++++++-- kitty/window.py | 11 +++--- 11 files changed, 184 insertions(+), 12 deletions(-) diff --git a/README.asciidoc b/README.asciidoc index 5a46305cf..d91fb3891 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -48,6 +48,7 @@ :sc_seventh_window: pass:quotes[`ctrl+shift+7`] :sc_show_scrollback: pass:quotes[`ctrl+shift+h`] :sc_sixth_window: pass:quotes[`ctrl+shift+6`] +:sc_start_resizing_window: pass:quotes[`ctrl+shift+r`] :sc_tenth_window: pass:quotes[`ctrl+shift+0`] :sc_third_window: pass:quotes[`ctrl+shift+3`] :sc_toggle_fullscreen: pass:quotes[`ctrl+shift+f11`] @@ -343,6 +344,15 @@ You can switch between layouts using the {sc_next_layout} key combination. You c also create shortcuts to select particular layouts, and choose which layouts you want to enable/disable, see link:kitty/kitty.conf[kitty.conf] for examples. +You can resize windows inside layouts. Press {sc_start_resizing_window} to +enter resizing mode. Then use the `W/N` (Wider/Narrower) and `T/S` +(Taller/Shorter) keys to change the window size. Press the `0` key to rest the +layout to default sizes. Any other key will exit resize mode. In a given +window layout only some operations may be possible for a particular window. For +example, in the Tall layout you can make the first window wider/narrower, but +not taller/shorter. Note that what you are resizing is actually not a window, +but a row/column in the layout, all windows in that row/column will be resized. + Some layouts take options to control their behavior. For example, the `fat` and `tall` layouts accept the `bias` option to control how the available space is split up. To specify the option, in kitty.conf use: @@ -351,7 +361,7 @@ option, in kitty.conf use: enabled_layouts tall:bias=70 ``` -this will make the tall window occupy `70%` of available width. `bias` can be +This will make the tall window occupy `70%` of available width. `bias` can be any number between 10 and 90. Writing a new layout only requires about fifty lines of code, so if there is diff --git a/kitty/boss.py b/kitty/boss.py index 2f9f8ceb5..1dfce4b59 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -19,7 +19,8 @@ appname, config_dir, editor, set_boss, supports_primary_selection ) from .fast_data_types import ( - ChildMonitor, create_os_window, current_os_window, destroy_global_data, + GLFW_KEY_0, GLFW_KEY_W, GLFW_KEY_N, GLFW_KEY_S, ChildMonitor, + create_os_window, current_os_window, destroy_global_data, destroy_sprite_map, get_clipboard_string, glfw_post_empty_event, layout_sprite_map, mark_os_window_for_close, set_clipboard_string, set_dpi_from_os_window, set_in_sequence_mode, show_window, @@ -413,6 +414,22 @@ def process_sequence(self, key, scancode, action, mods): if matched_action is not None: self.dispatch_action(matched_action) + def handle_resize_keypress(self, key, mods, os_window_id, tab_id, window_id): + tm = self.os_window_map.get(os_window_id) + if tm is None: + return + tab = tm.tab_for_id(tab_id) + if tab is None: + return + if key == GLFW_KEY_0: + tab.reset_window_sizes() + return + is_horizontal = key in (GLFW_KEY_W, GLFW_KEY_N) + increment = 0.05 + if key in (GLFW_KEY_N, GLFW_KEY_S): + increment *= -1 + tab.resize_window_by(window_id, increment, is_horizontal) + def default_bg_changed_for(self, window_id): w = self.window_id_map.get(window_id) if w is not None: diff --git a/kitty/glfw.c b/kitty/glfw.c index f1b4a59d5..1b77e3194 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -765,6 +765,16 @@ os_window_swap_buffers(PyObject UNUSED *self, PyObject *args) { return NULL; } +static PyObject* +ring_bell(PyObject UNUSED *self, PyObject *args) { + id_type os_window_id; + if (!PyArg_ParseTuple(args, "K", &os_window_id)) return NULL; + OSWindow *w = os_window_for_kitty_window(os_window_id); + if (w && w->handle) { + glfwWindowBell(w->handle); + } + Py_RETURN_NONE; +} // Boilerplate {{{ static PyMethodDef module_methods[] = { @@ -778,6 +788,7 @@ static PyMethodDef module_methods[] = { METHODB(glfw_window_hint, METH_VARARGS), METHODB(os_window_should_close, METH_VARARGS), METHODB(os_window_swap_buffers, METH_VARARGS), + METHODB(ring_bell, METH_VARARGS), METHODB(get_primary_selection, METH_NOARGS), METHODB(x11_display, METH_NOARGS), METHODB(x11_window_id, METH_O), diff --git a/kitty/keys.c b/kitty/keys.c index 2930d1364..b050048f1 100644 --- a/kitty/keys.c +++ b/kitty/keys.c @@ -81,10 +81,24 @@ is_ascii_control_char(char c) { return c == 0 || (1 <= c && c <= 31) || c == 127; } +static inline bool +handle_resize_key(int key, int action, int mods) { + if (action == GLFW_RELEASE) return true; + if (key == GLFW_KEY_T || key == GLFW_KEY_S || key == GLFW_KEY_W || key == GLFW_KEY_N || key == GLFW_KEY_0) { + call_boss(handle_resize_keypress, "iiKKK", key, mods, global_state.currently_resizing.os_window_id, global_state.currently_resizing.tab_id, global_state.currently_resizing.window_id); + return true; + } + return false; +} + void on_key_input(int key, int scancode, int action, int mods, const char* text, int state UNUSED) { Window *w = active_window(); if (!w) return; + if (global_state.currently_resizing.os_window_id) { + if (handle_resize_key(key, action, mods)) return; + terminate_resize_mode(); + } if (global_state.in_sequence_mode) { if ( action != GLFW_RELEASE && diff --git a/kitty/kitty.conf b/kitty/kitty.conf index 6c3cfd0af..9df2db270 100644 --- a/kitty/kitty.conf +++ b/kitty/kitty.conf @@ -395,6 +395,8 @@ map kitty_mod+[ previous_window map kitty_mod+f move_window_forward map kitty_mod+b move_window_backward map kitty_mod+` move_window_to_top +map kitty_mod+r start_resizing_window +# Switching to a particular window map kitty_mod+1 first_window map kitty_mod+2 second_window map kitty_mod+3 third_window diff --git a/kitty/layout.py b/kitty/layout.py index bb2671b80..0be8ab7de 100644 --- a/kitty/layout.py +++ b/kitty/layout.py @@ -4,7 +4,7 @@ from collections import namedtuple from functools import partial -from itertools import islice +from itertools import islice, repeat from .constants import WindowGeometry from .fast_data_types import ( @@ -42,8 +42,15 @@ def calc_window_length(cells_in_window): inner_length = cells_in_window * cell_length return 2 * (border_length + margin_length) + inner_length - if bias is not None and number_of_windows > 1 and len(bias) == number_of_windows: + if bias is not None and number_of_windows > 1 and len(bias) == number_of_windows and cells_per_window > 5: cells_map = [int(b * number_of_cells) for b in bias] + while min(cells_map) < 5: + maxi, mini = map(cells_map.index, (max(cells_map), min(cells_map))) + if maxi == mini: + break + cells_map[mini] += 1 + cells_map[maxi] -= 1 + extra = number_of_cells - sum(cells_map) if extra: cells_map[-1] += extra @@ -90,6 +97,28 @@ def __init__(self, os_window_id, tab_id, margin_width, padding_width, border_wid self.blank_rects = () self.layout_opts = self.parse_layout_opts(layout_opts) self.full_name = self.name + ((':' + layout_opts) if layout_opts else '') + self.initialize_sub_class() + + def initialize_sub_class(self): + pass + + def apply_bias(self, idx, increment, num_windows, is_horizontal): + return False + + def remove_all_biases(self): + return False + + def modify_size_of_window(self, all_windows, window_id, increment, is_horizontal=True): + idx = idx_for_id(window_id, all_windows) + if idx is None: + return + w = all_windows[idx] + windows = process_overlaid_windows(all_windows)[1] + idx = idx_for_id(w.id, windows) + if idx is None: + idx = idx_for_id(w.overlay_window_id, windows) + if idx is not None: + return self.apply_bias(idx, increment, len(windows), is_horizontal) def parse_layout_opts(self, layout_opts): if not layout_opts: @@ -307,10 +336,59 @@ def do_layout(self, windows, active_window_idx): self.blank_rects = blank_rects_for_window(w) +def safe_increment_bias(old_val, increment): + return max(0.1, min(old_val + increment, 0.9)) + + +def normalize_biases(biases): + s = sum(biases) + if s == 1: + return biases + return [x/s for x in biases] + + +def distribute_indexed_bias(base_bias, index_bias_map): + if not index_bias_map: + return base_bias + ans = list(base_bias) + limit = len(ans) + for row, increment in index_bias_map.items(): + if row >= limit or not increment: + continue + other_increment = -increment / (limit - 1) + ans = [safe_increment_bias(b, increment if i == row else other_increment) for i, b in enumerate(ans)] + return normalize_biases(ans) + + class Tall(Layout): name = 'tall' + def initialize_sub_class(self): + self.remove_all_biases() + + def remove_all_biases(self): + self.x_bias = list(self.layout_opts['bias']) + self.biased_rows = {} + return True + + def apply_bias(self, idx, increment, num_windows, is_horizontal): + if is_horizontal: + before = self.x_bias + if idx == 0: + self.x_bias = [safe_increment_bias(self.x_bias[0], increment), safe_increment_bias(self.x_bias[1], -increment)] + else: + self.x_bias = [safe_increment_bias(self.x_bias[0], -increment), safe_increment_bias(self.x_bias[1], increment)] + self.x_bias = normalize_biases(self.x_bias) + after = self.x_bias + else: + if idx == 0: + return False + idx -= 1 + before = self.biased_rows.get(idx, 0) + self.biased_rows[idx] = after = before + increment + return before != after + def parse_layout_opts(self, layout_opts): ans = Layout.parse_layout_opts(self, layout_opts) try: @@ -328,12 +406,16 @@ def do_layout(self, windows, active_window_idx): windows[0].set_geometry(0, wg) self.blank_rects = blank_rects_for_window(windows[0]) return - xlayout = self.xlayout(2, bias=self.layout_opts['bias']) + xlayout = self.xlayout(2, bias=self.x_bias) xstart, xnum = next(xlayout) ystart, ynum = next(self.ylayout(1)) windows[0].set_geometry(0, window_geometry(xstart, xnum, ystart, ynum)) xstart, xnum = next(xlayout) - ylayout = self.ylayout(len(windows) - 1) + if len(windows) > 2: + y_bias = distribute_indexed_bias(list(repeat(1/(len(windows) - 1), len(windows) - 1)), self.biased_rows) + else: + y_bias = None + ylayout = self.ylayout(len(windows) - 1, bias=y_bias) for i, (w, (ystart, ynum)) in enumerate(zip(islice(windows, 1, None), ylayout)): w.set_geometry(i + 1, window_geometry(xstart, xnum, ystart, ynum)) # right bottom blank rect diff --git a/kitty/mouse.c b/kitty/mouse.c index 1dc0c9cb7..90d125001 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -303,6 +303,7 @@ open_url(Window *w) { } HANDLER(handle_button_event) { + if (global_state.currently_resizing.os_window_id) { terminate_resize_mode(); } Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab; bool is_release = !global_state.callback_os_window->mouse_button_pressed[button]; if (window_idx != t->active_window) { diff --git a/kitty/state.c b/kitty/state.c index a9f1caa4e..6246b2961 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -268,6 +268,14 @@ os_window_regions(OSWindow *os_window, Region *central, Region *tab_bar) { } } +void +terminate_resize_mode() { + global_state.currently_resizing.os_window_id = 0; + global_state.currently_resizing.tab_id = 0; + global_state.currently_resizing.window_id = 0; +} + + // Python API {{{ #define PYWRAP0(name) static PyObject* py##name(PYNOARG) @@ -586,6 +594,14 @@ PYWRAP1(set_display_state) { Py_RETURN_NONE; } +PYWRAP1(enter_resize_mode) { + global_state.currently_resizing.os_window_id = 0; + global_state.currently_resizing.tab_id = 0; + global_state.currently_resizing.window_id = 0; + PA("|KKK", &global_state.currently_resizing.os_window_id, &global_state.currently_resizing.tab_id, &global_state.currently_resizing.window_id); + Py_RETURN_NONE; +} + THREE_ID_OBJ(update_window_title) THREE_ID(remove_window) PYWRAP1(resolve_key_mods) { int mods; PA("ii", &kitty_mod, &mods); return PyLong_FromLong(resolve_mods(mods)); } @@ -632,6 +648,7 @@ static PyMethodDef module_methods[] = { MW(update_window_visibility, METH_VARARGS), MW(set_boss, METH_O), MW(set_display_state, METH_VARARGS), + MW(enter_resize_mode, METH_VARARGS), MW(destroy_global_data, METH_NOARGS), {NULL, NULL, 0, NULL} /* Sentinel */ diff --git a/kitty/state.h b/kitty/state.h index a72dac8d0..9d71714e6 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -137,6 +137,9 @@ typedef struct { bool debug_gl, debug_font_fallback; bool has_pending_resizes; bool in_sequence_mode; + struct { + id_type os_window_id, tab_id, window_id; + } currently_resizing; } GlobalState; extern GlobalState global_state; @@ -183,3 +186,4 @@ void free_texture(uint32_t*); void send_image_to_gpu(uint32_t*, const void*, int32_t, int32_t, bool, bool); void send_sprite_to_gpu(unsigned int, unsigned int, unsigned int, pixel*); void set_titlebar_color(OSWindow *w, color_type color); +void terminate_resize_mode(); diff --git a/kitty/tabs.py b/kitty/tabs.py index 0ebfe84da..df47d79e3 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -12,8 +12,9 @@ from .constants import WindowGeometry, appname, get_boss, is_macos, is_wayland from .fast_data_types import ( DECAWM, Screen, add_tab, glfw_post_empty_event, mark_tab_bar_dirty, - next_window_id, pt_to_px, remove_tab, remove_window, set_active_tab, - set_tab_bar_render_data, swap_tabs, viewport_for_window, x11_window_id + next_window_id, pt_to_px, remove_tab, remove_window, ring_bell, + set_active_tab, set_tab_bar_render_data, swap_tabs, viewport_for_window, + x11_window_id ) from .layout import Rect, create_layout_object_for, evict_cached_layouts from .session import resolved_shell @@ -162,6 +163,16 @@ def goto_layout(self, layout_name): self.current_layout = self.create_layout_object(layout_name) self.relayout() + def resize_window_by(self, window_id, increment, is_horizontal): + if self.current_layout.modify_size_of_window(self.windows, window_id, increment, is_horizontal): + self.relayout() + else: + ring_bell(self.os_window_id) + + def reset_window_sizes(self): + if self.current_layout.remove_all_biases(): + self.relayout() + def launch_child(self, use_shell=False, cmd=None, stdin=None, cwd_from=None, cwd=None, env=None): if cmd is None: if use_shell: diff --git a/kitty/window.py b/kitty/window.py index 95ea1dc67..ed2dad038 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -17,10 +17,10 @@ BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM, CELL_PROGRAM, CELL_SPECIAL_PROGRAM, CSI, CURSOR_PROGRAM, DCS, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_PROGRAM, OSC, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, Screen, - add_window, compile_program, get_clipboard_string, glfw_post_empty_event, - init_cell_program, init_cursor_program, set_clipboard_string, - set_titlebar_color, set_window_render_data, update_window_title, - update_window_visibility, viewport_for_window + add_window, compile_program, enter_resize_mode, get_clipboard_string, + glfw_post_empty_event, init_cell_program, init_cursor_program, + set_clipboard_string, set_titlebar_color, set_window_render_data, + update_window_title, update_window_visibility, viewport_for_window ) from .keys import keyboard_mode_name from .rgb import to_color @@ -193,6 +193,9 @@ def send_text(self, *args): return True self.write_to_child(text) + def start_resizing_window(self): + enter_resize_mode(self.os_window_id, self.tab_id, self.id) + def write_to_child(self, data): if data: if get_boss().child_monitor.needs_write(self.id, data) is not True: