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.
This commit is contained in:
Kovid Goyal 2018-05-17 15:09:41 +05:30
parent e429b8484c
commit e053d1f566
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
11 changed files with 184 additions and 12 deletions

View File

@ -48,6 +48,7 @@
:sc_seventh_window: pass:quotes[`ctrl+shift+7`] :sc_seventh_window: pass:quotes[`ctrl+shift+7`]
:sc_show_scrollback: pass:quotes[`ctrl+shift+h`] :sc_show_scrollback: pass:quotes[`ctrl+shift+h`]
:sc_sixth_window: pass:quotes[`ctrl+shift+6`] :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_tenth_window: pass:quotes[`ctrl+shift+0`]
:sc_third_window: pass:quotes[`ctrl+shift+3`] :sc_third_window: pass:quotes[`ctrl+shift+3`]
:sc_toggle_fullscreen: pass:quotes[`ctrl+shift+f11`] :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 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 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` 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 layouts accept the `bias` option to control how the available space is split up. To specify the
option, in kitty.conf use: option, in kitty.conf use:
@ -351,7 +361,7 @@ option, in kitty.conf use:
enabled_layouts tall:bias=70 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. any number between 10 and 90.
Writing a new layout only requires about fifty lines of code, so if there is Writing a new layout only requires about fifty lines of code, so if there is

View File

@ -19,7 +19,8 @@
appname, config_dir, editor, set_boss, supports_primary_selection appname, config_dir, editor, set_boss, supports_primary_selection
) )
from .fast_data_types import ( 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, destroy_sprite_map, get_clipboard_string, glfw_post_empty_event,
layout_sprite_map, mark_os_window_for_close, set_clipboard_string, layout_sprite_map, mark_os_window_for_close, set_clipboard_string,
set_dpi_from_os_window, set_in_sequence_mode, show_window, 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: if matched_action is not None:
self.dispatch_action(matched_action) 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): def default_bg_changed_for(self, window_id):
w = self.window_id_map.get(window_id) w = self.window_id_map.get(window_id)
if w is not None: if w is not None:

View File

@ -765,6 +765,16 @@ os_window_swap_buffers(PyObject UNUSED *self, PyObject *args) {
return NULL; 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 {{{ // Boilerplate {{{
static PyMethodDef module_methods[] = { static PyMethodDef module_methods[] = {
@ -778,6 +788,7 @@ static PyMethodDef module_methods[] = {
METHODB(glfw_window_hint, METH_VARARGS), METHODB(glfw_window_hint, METH_VARARGS),
METHODB(os_window_should_close, METH_VARARGS), METHODB(os_window_should_close, METH_VARARGS),
METHODB(os_window_swap_buffers, METH_VARARGS), METHODB(os_window_swap_buffers, METH_VARARGS),
METHODB(ring_bell, METH_VARARGS),
METHODB(get_primary_selection, METH_NOARGS), METHODB(get_primary_selection, METH_NOARGS),
METHODB(x11_display, METH_NOARGS), METHODB(x11_display, METH_NOARGS),
METHODB(x11_window_id, METH_O), METHODB(x11_window_id, METH_O),

View File

@ -81,10 +81,24 @@ is_ascii_control_char(char c) {
return c == 0 || (1 <= c && c <= 31) || c == 127; 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 void
on_key_input(int key, int scancode, int action, int mods, const char* text, int state UNUSED) { on_key_input(int key, int scancode, int action, int mods, const char* text, int state UNUSED) {
Window *w = active_window(); Window *w = active_window();
if (!w) return; 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 (global_state.in_sequence_mode) {
if ( if (
action != GLFW_RELEASE && action != GLFW_RELEASE &&

View File

@ -395,6 +395,8 @@ map kitty_mod+[ previous_window
map kitty_mod+f move_window_forward map kitty_mod+f move_window_forward
map kitty_mod+b move_window_backward map kitty_mod+b move_window_backward
map kitty_mod+` move_window_to_top 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+1 first_window
map kitty_mod+2 second_window map kitty_mod+2 second_window
map kitty_mod+3 third_window map kitty_mod+3 third_window

View File

@ -4,7 +4,7 @@
from collections import namedtuple from collections import namedtuple
from functools import partial from functools import partial
from itertools import islice from itertools import islice, repeat
from .constants import WindowGeometry from .constants import WindowGeometry
from .fast_data_types import ( from .fast_data_types import (
@ -42,8 +42,15 @@ def calc_window_length(cells_in_window):
inner_length = cells_in_window * cell_length inner_length = cells_in_window * cell_length
return 2 * (border_length + margin_length) + inner_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] 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) extra = number_of_cells - sum(cells_map)
if extra: if extra:
cells_map[-1] += 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.blank_rects = ()
self.layout_opts = self.parse_layout_opts(layout_opts) self.layout_opts = self.parse_layout_opts(layout_opts)
self.full_name = self.name + ((':' + layout_opts) if layout_opts else '') 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): def parse_layout_opts(self, layout_opts):
if not 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) 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): class Tall(Layout):
name = 'tall' 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): def parse_layout_opts(self, layout_opts):
ans = Layout.parse_layout_opts(self, layout_opts) ans = Layout.parse_layout_opts(self, layout_opts)
try: try:
@ -328,12 +406,16 @@ def do_layout(self, windows, active_window_idx):
windows[0].set_geometry(0, wg) windows[0].set_geometry(0, wg)
self.blank_rects = blank_rects_for_window(windows[0]) self.blank_rects = blank_rects_for_window(windows[0])
return return
xlayout = self.xlayout(2, bias=self.layout_opts['bias']) xlayout = self.xlayout(2, bias=self.x_bias)
xstart, xnum = next(xlayout) xstart, xnum = next(xlayout)
ystart, ynum = next(self.ylayout(1)) ystart, ynum = next(self.ylayout(1))
windows[0].set_geometry(0, window_geometry(xstart, xnum, ystart, ynum)) windows[0].set_geometry(0, window_geometry(xstart, xnum, ystart, ynum))
xstart, xnum = next(xlayout) 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)): for i, (w, (ystart, ynum)) in enumerate(zip(islice(windows, 1, None), ylayout)):
w.set_geometry(i + 1, window_geometry(xstart, xnum, ystart, ynum)) w.set_geometry(i + 1, window_geometry(xstart, xnum, ystart, ynum))
# right bottom blank rect # right bottom blank rect

View File

@ -303,6 +303,7 @@ open_url(Window *w) {
} }
HANDLER(handle_button_event) { 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; 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]; bool is_release = !global_state.callback_os_window->mouse_button_pressed[button];
if (window_idx != t->active_window) { if (window_idx != t->active_window) {

View File

@ -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 {{{ // Python API {{{
#define PYWRAP0(name) static PyObject* py##name(PYNOARG) #define PYWRAP0(name) static PyObject* py##name(PYNOARG)
@ -586,6 +594,14 @@ PYWRAP1(set_display_state) {
Py_RETURN_NONE; 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_OBJ(update_window_title)
THREE_ID(remove_window) THREE_ID(remove_window)
PYWRAP1(resolve_key_mods) { int mods; PA("ii", &kitty_mod, &mods); return PyLong_FromLong(resolve_mods(mods)); } 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(update_window_visibility, METH_VARARGS),
MW(set_boss, METH_O), MW(set_boss, METH_O),
MW(set_display_state, METH_VARARGS), MW(set_display_state, METH_VARARGS),
MW(enter_resize_mode, METH_VARARGS),
MW(destroy_global_data, METH_NOARGS), MW(destroy_global_data, METH_NOARGS),
{NULL, NULL, 0, NULL} /* Sentinel */ {NULL, NULL, 0, NULL} /* Sentinel */

View File

@ -137,6 +137,9 @@ typedef struct {
bool debug_gl, debug_font_fallback; bool debug_gl, debug_font_fallback;
bool has_pending_resizes; bool has_pending_resizes;
bool in_sequence_mode; bool in_sequence_mode;
struct {
id_type os_window_id, tab_id, window_id;
} currently_resizing;
} GlobalState; } GlobalState;
extern GlobalState global_state; 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_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 send_sprite_to_gpu(unsigned int, unsigned int, unsigned int, pixel*);
void set_titlebar_color(OSWindow *w, color_type color); void set_titlebar_color(OSWindow *w, color_type color);
void terminate_resize_mode();

View File

@ -12,8 +12,9 @@
from .constants import WindowGeometry, appname, get_boss, is_macos, is_wayland from .constants import WindowGeometry, appname, get_boss, is_macos, is_wayland
from .fast_data_types import ( from .fast_data_types import (
DECAWM, Screen, add_tab, glfw_post_empty_event, mark_tab_bar_dirty, 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, next_window_id, pt_to_px, remove_tab, remove_window, ring_bell,
set_tab_bar_render_data, swap_tabs, viewport_for_window, x11_window_id 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 .layout import Rect, create_layout_object_for, evict_cached_layouts
from .session import resolved_shell 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.current_layout = self.create_layout_object(layout_name)
self.relayout() 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): def launch_child(self, use_shell=False, cmd=None, stdin=None, cwd_from=None, cwd=None, env=None):
if cmd is None: if cmd is None:
if use_shell: if use_shell:

View File

@ -17,10 +17,10 @@
BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM, CELL_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM, CELL_PROGRAM,
CELL_SPECIAL_PROGRAM, CSI, CURSOR_PROGRAM, DCS, GRAPHICS_PREMULT_PROGRAM, CELL_SPECIAL_PROGRAM, CSI, CURSOR_PROGRAM, DCS, GRAPHICS_PREMULT_PROGRAM,
GRAPHICS_PROGRAM, OSC, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, Screen, GRAPHICS_PROGRAM, OSC, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, Screen,
add_window, compile_program, get_clipboard_string, glfw_post_empty_event, add_window, compile_program, enter_resize_mode, get_clipboard_string,
init_cell_program, init_cursor_program, set_clipboard_string, glfw_post_empty_event, init_cell_program, init_cursor_program,
set_titlebar_color, set_window_render_data, update_window_title, set_clipboard_string, set_titlebar_color, set_window_render_data,
update_window_visibility, viewport_for_window update_window_title, update_window_visibility, viewport_for_window
) )
from .keys import keyboard_mode_name from .keys import keyboard_mode_name
from .rgb import to_color from .rgb import to_color
@ -193,6 +193,9 @@ def send_text(self, *args):
return True return True
self.write_to_child(text) 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): def write_to_child(self, data):
if data: if data:
if get_boss().child_monitor.needs_write(self.id, data) is not True: if get_boss().child_monitor.needs_write(self.id, data) is not True: