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_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

View File

@ -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:

View File

@ -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),

View File

@ -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 &&

View File

@ -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

View File

@ -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

View File

@ -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) {

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 {{{
#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 */

View File

@ -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();

View File

@ -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:

View File

@ -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: