mirror of
https://github.com/kovidgoyal/kitty.git
synced 2024-08-16 18:10:32 +03:00
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:
parent
e429b8484c
commit
e053d1f566
@ -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
|
||||
|
@ -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:
|
||||
|
11
kitty/glfw.c
11
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),
|
||||
|
14
kitty/keys.c
14
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 &&
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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 */
|
||||
|
@ -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();
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user