diff --git a/README.md b/README.md index 46cd07cc..184854de 100644 --- a/README.md +++ b/README.md @@ -61,18 +61,6 @@ This is a list of known, but probably not all, issues: Examples: á (`LATIN SMALL LETTER A` + `COMBINING ACUTE ACCENT`) -* GNOME; might work, but without window decorations. - - Strictly speaking, foot is at fault here; all Wayland applications - _must_ be able to draw their own window decorations (but foot is - not). - - However, most people want a uniform look and feel on their - desktop, including the window decorations. For this reason, a - Wayland application can request _Server Side Decorations_ - (SSD). GNOME will reply with a "_I hear you, but sorry, I wont do - that_". - ## Fonts diff --git a/completions/zsh/_foot b/completions/zsh/_foot index ad04a467..c573d4cf 100644 --- a/completions/zsh/_foot +++ b/completions/zsh/_foot @@ -6,7 +6,7 @@ _arguments \ '(-f --font)'{-f,--font}'[font name and style in fontconfig format (monospace)]:font:->fonts' \ '(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \ '--login-shell[start shell as a login shell]' \ - '(-g --geometry)'{-g,--geometry}'[window WIDTHxHEIGHT, in pixels (80x24 cells)]:geometry:()' \ + '(-g --geometry)'{-g,--geometry}'[window WIDTHxHEIGHT, in pixels (700x50)]:geometry:()' \ '(-s --server)'{-s,--server}'[run as server; open terminals by running footclient]:server:_files' \ '--hold[remain open after child process exits]' \ '(-p --print-pid)'{-p,--print-pid}'[print PID to this file or FD when up and running (server mode only)]:pidfile:_files' \ diff --git a/config.c b/config.c index 39a72648..46056567 100644 --- a/config.c +++ b/config.c @@ -44,15 +44,19 @@ static const uint32_t default_bright[] = { static char * get_shell(void) { - struct passwd *passwd = getpwuid(getuid()); - if (passwd == NULL) { - LOG_ERRNO("failed to lookup user"); - return NULL; + const char *shell = getenv("SHELL"); + + if (shell == NULL) { + struct passwd *passwd = getpwuid(getuid()); + if (passwd == NULL) { + LOG_ERRNO("failed to lookup user"); + return NULL; + } + + shell = passwd->pw_shell; } - const char *shell = passwd->pw_shell; LOG_DBG("user's shell: %s", shell); - return strdup(shell); } @@ -133,7 +137,7 @@ str_to_double(const char *s, double *res) } static bool -str_to_color(const char *s, uint32_t *color, const char *path, int lineno) +str_to_color(const char *s, uint32_t *color, bool allow_alpha, const char *path, int lineno) { unsigned long value; if (!str_to_ulong(s, 16, &value)) { @@ -141,7 +145,12 @@ str_to_color(const char *s, uint32_t *color, const char *path, int lineno) return false; } - *color = value & 0xffffff; + if (!allow_alpha && (value & 0xff000000) != 0) { + LOG_ERR("%s:%d: color value must not have an alpha component", path, lineno); + return false; + } + + *color = value; return true; } @@ -273,7 +282,7 @@ parse_section_colors(const char *key, const char *value, struct config *conf, } uint32_t color_value; - if (!str_to_color(value, &color_value, path, lineno)) + if (!str_to_color(value, &color_value, false, path, lineno)) return false; *color = color_value; @@ -305,8 +314,8 @@ parse_section_cursor(const char *key, const char *value, struct config *conf, uint32_t text_color, cursor_color; if (text == NULL || cursor == NULL || - !str_to_color(text, &text_color, path, lineno) || - !str_to_color(cursor, &cursor_color, path, lineno)) + !str_to_color(text, &text_color, false, path, lineno) || + !str_to_color(cursor, &cursor_color, false, path, lineno)) { LOG_ERR("%s:%d: invalid cursor colors: %s", path, lineno, value); free(value_copy); @@ -326,6 +335,87 @@ parse_section_cursor(const char *key, const char *value, struct config *conf, return true; } +static bool +parse_section_csd(const char *key, const char *value, struct config *conf, + const char *path, unsigned lineno) +{ + if (strcmp(key, "preferred") == 0) { + if (strcmp(value, "server") == 0) + conf->csd.preferred = CONF_CSD_PREFER_SERVER; + else if (strcmp(value, "client") == 0) + conf->csd.preferred = CONF_CSD_PREFER_CLIENT; + else { + LOG_ERR("%s:%d: expected either 'server' or 'client'", path, lineno); + return false; + } + } + + else if (strcmp(key, "color") == 0) { + uint32_t color; + if (!str_to_color(value, &color, true, path, lineno)) { + LOG_ERR("%s:%d: invalid titlebar-color: %s", path, lineno, value); + return false; + } + + conf->csd.color.title_set = true; + conf->csd.color.title = color; + } + + else if (strcmp(key, "size") == 0) { + unsigned long pixels; + if (!str_to_ulong(value, 10, &pixels)) { + LOG_ERR("%s:%d: expected an integer: %s", path, lineno, value); + return false; + } + + conf->csd.title_height = pixels; + } + + else if (strcmp(key, "button-width") == 0) { + unsigned long pixels; + if (!str_to_ulong(value, 10, &pixels)) { + LOG_ERR("%s:%d: expected an integer: %s", path, lineno, value); + return false; + } + + conf->csd.button_width = pixels; + } + + else if (strcmp(key, "button-minimize-color") == 0) { + uint32_t color; + if (!str_to_color(value, &color, true, path, lineno)) { + LOG_ERR("%s:%d: invalid button-minimize-color: %s", path, lineno, value); + return false; + } + + conf->csd.color.minimize_set = true; + conf->csd.color.minimize = color; + } + + else if (strcmp(key, "button-maximize-color") == 0) { + uint32_t color; + if (!str_to_color(value, &color, true, path, lineno)) { + LOG_ERR("%s:%d: invalid button-maximize-color: %s", path, lineno, value); + return false; + } + + conf->csd.color.maximize_set = true; + conf->csd.color.maximize = color; + } + + else if (strcmp(key, "button-close-color") == 0) { + uint32_t color; + if (!str_to_color(value, &color, true, path, lineno)) { + LOG_ERR("%s:%d: invalid button-close-color: %s", path, lineno, value); + return false; + } + + conf->csd.color.close_set = true; + conf->csd.color.close = color; + } + return true; +} + static bool parse_config_file(FILE *f, struct config *conf, const char *path) { @@ -333,6 +423,7 @@ parse_config_file(FILE *f, struct config *conf, const char *path) SECTION_MAIN, SECTION_COLORS, SECTION_CURSOR, + SECTION_CSD, } section = SECTION_MAIN; /* Function pointer, called for each key/value line */ @@ -345,6 +436,7 @@ parse_config_file(FILE *f, struct config *conf, const char *path) [SECTION_MAIN] = &parse_section_main, [SECTION_COLORS] = &parse_section_colors, [SECTION_CURSOR] = &parse_section_cursor, + [SECTION_CSD] = &parse_section_csd, }; #if defined(_DEBUG) && defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG @@ -352,25 +444,25 @@ parse_config_file(FILE *f, struct config *conf, const char *path) [SECTION_MAIN] = "main", [SECTION_COLORS] = "colors", [SECTION_CURSOR] = "cursor", + [SECTION_CSD] = "csd", }; #endif unsigned lineno = 0; + char *_line = NULL; + size_t count = 0; while (true) { errno = 0; lineno++; - _line = NULL; - size_t count = 0; ssize_t ret = getline(&_line, &count, f); if (ret < 0) { - free(_line); if (errno != 0) { LOG_ERRNO("failed to read from configuration"); - return false; + goto err; } break; } @@ -389,10 +481,8 @@ parse_config_file(FILE *f, struct config *conf, const char *path) } /* Empty line, or comment */ - if (line[0] == '\0' || line[0] == '#') { - free(_line); + if (line[0] == '\0' || line[0] == '#') continue; - } /* Check for new section */ if (line[0] == '[') { @@ -408,12 +498,13 @@ parse_config_file(FILE *f, struct config *conf, const char *path) section = SECTION_COLORS; else if (strcmp(&line[1], "cursor") == 0) section = SECTION_CURSOR; + else if (strcmp(&line[1], "csd") == 0) + section = SECTION_CSD; else { LOG_ERR("%s:%d: invalid section name: %s", path, lineno, &line[1]); goto err; } - free(_line); continue; } @@ -436,7 +527,6 @@ parse_config_file(FILE *f, struct config *conf, const char *path) goto err; } - free(_line); continue; } @@ -447,10 +537,8 @@ parse_config_file(FILE *f, struct config *conf, const char *path) assert(!isspace(*(value + strlen(value) - 1))); } - if (key[0] == '#') { - free(_line); + if (key[0] == '#') continue; - } LOG_DBG("section=%s, key='%s', value='%s'", section_names[section], key, value); @@ -460,10 +548,9 @@ parse_config_file(FILE *f, struct config *conf, const char *path) if (!section_parser(key, value, conf, path, lineno)) goto err; - - free(_line); } + free(_line); return true; err: @@ -491,8 +578,8 @@ config_load(struct config *conf, const char *conf_path) *conf = (struct config) { .term = strdup("foot"), .shell = get_shell(), - .width = -1, - .height = -1, + .width = 700, + .height = 500, .pad_x = 2, .pad_y = 2, .fonts = tll_init(), @@ -532,6 +619,13 @@ config_load(struct config *conf, const char *conf_path) }, }, + .csd = { + .preferred = CONF_CSD_PREFER_SERVER, + .title_height = 26, + .border_width = 5, + .button_width = 26, + }, + .render_worker_count = sysconf(_SC_NPROCESSORS_ONLN), .server_socket_path = get_server_socket_path(), .presentation_timings = false, @@ -562,10 +656,10 @@ config_load(struct config *conf, const char *conf_path) ret = parse_config_file(f, conf, conf_path); fclose(f); +out: if (ret && tll_length(conf->fonts) == 0) tll_push_back(conf->fonts, strdup("monospace")); -out: free(default_path); return ret; } diff --git a/config.h b/config.h index 3677129f..a89637e1 100644 --- a/config.h +++ b/config.h @@ -36,6 +36,25 @@ struct config { } color; } cursor; + struct { + enum { CONF_CSD_PREFER_SERVER, CONF_CSD_PREFER_CLIENT } preferred; + + int title_height; + int border_width; + int button_width; + + struct { + bool title_set; + bool minimize_set; + bool maximize_set; + bool close_set; + uint32_t title; + uint32_t minimize; + uint32_t maximize; + uint32_t close; + } color; + } csd; + size_t render_worker_count; char *server_socket_path; bool presentation_timings; diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 8b5268b7..5b3c07cc 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -31,7 +31,7 @@ execute (instead of the shell). Default: _monospace_. *-g*,*--geometry*=_WIDTHxHEIGHT_ - Set initial window width and height. + Set initial window width and height. Default: *700x500*. *-t*,*--term*=_TERM_ Value to set the environment variable *TERM* to. Default: *foot*. diff --git a/doc/foot.5.scd b/doc/foot.5.scd index 41785366..edfecc27 100644 --- a/doc/foot.5.scd +++ b/doc/foot.5.scd @@ -41,9 +41,10 @@ in this order: _XxY_ (-padding). *shell* - Executable to launch. Typically a shell. Default: the user's - default shell (as specified in _/etc/passwd_). You can also pass - arguments. For example "/bin/bash --norc". + Executable to launch. Typically a shell. Default: _$SHELL_ if set, + otherwise the user's default shell (as specified in + _/etc/passwd_). You can also pass arguments. For example + "/bin/bash --norc". *login-shell* Start a login shell, by prepending a '-' to argv[0]. Default: _no_. @@ -63,6 +64,9 @@ in this order: # SECTION: cursor +This section controls the cursor style and color. Note that +applications can change these runtime. + *style* Configures the default cursor style, and is one of: _block_, _bar_ or _underline_. Default: _block_. @@ -76,6 +80,13 @@ in this order: # SECTION: colors +This section controls the 16 ANSI colors and the default foreground +and background colors. Note that applications can change these runtime. + +The colors are in RRGGBB format. That is, they do *not* have an alpha +component. You can configure the background transparency with the +_alpha_ option. + *foreground* Default RRGGBB foreground color. This is the color used when no ANSI color is being used. Default: _dcdccc_. @@ -98,6 +109,44 @@ in this order: Background translucency. A value in the range 0.0-1.0, where 0.0 means completely transparent, and 1.0 is opaque. Default: _1.0_. +# SECTION: csd + +This section controls the look of the _CSDs_ (Client Side +Decorations). Note that the default is to *not* use CSDs, but instead +to use _SSDs_ (Server Side Decorations) when the compositor supports +it. + +Note that unlike the colors defined in the _colors_ section, the color +values here are in AARRGGBB format. I.e. they contain an alpha +component. + +*preferred* + Which type of window decorations to prefer: *client* (CSD) or + *server* (SSD). Note that this is only a hint to the + compositor. Depending on the compositor's configuration and + capabilities, it may not have any effect. Default: _server_. + +*size* + Height, in pixels (subject to output scaling), of the + titlebar. Default: _26_. + +*color* + Titlebar AARRGGBB color. Default: use the default _foreground_ + color. + +*button-width* + Width, in pixels (subject to output scaling), of the + minimize/maximize/close buttons. Default: _26_. + +*button-minimize-color* + Minimize button's AARRGGBB color. Default: _ff1e90ff_. + +*button-maximize-color* + Maximize button's AARRGGBB color. Default: _ff30ff30_. + +*button-close-color* + Close button's AARRGGBB color. Default: _ffff3030_. + # FONT FORMAT The font is specified in FontConfig syntax. That is, a colon-separated diff --git a/footrc b/footrc index 337be31b..7cac547d 100644 --- a/footrc +++ b/footrc @@ -2,9 +2,9 @@ # font=monospace # scrollback=1000 -# geometry=500x300 +# geometry=700x500 # pad=2x2 -# shell= (you may need to override if you need a login shell) +# shell=$SHELL (if set, otherwise user's default shell from /etc/passwd) # term=foot # login-shell=no # workers= @@ -14,6 +14,7 @@ # color=111111 dcdccc [colors] +# alpha=1.0 # foreground=dcdccc # background=111111 # regular0=222222 @@ -32,4 +33,12 @@ # bright5=fcace3 # bright6=b3ffff # bright7=ffffff -# alpha=1.0 + +[csd] +# preferred=server +# size=26 +# color= +# button-width=26 +# button-minimize-color=ff0000ff +# button-maximize-color=ff00ff00 +# button-close-color=ffff0000 diff --git a/grid.c b/grid.c index 463dca7e..d538edc9 100644 --- a/grid.c +++ b/grid.c @@ -60,7 +60,7 @@ grid_reflow(struct grid *grid, int new_rows, int new_cols, const int old_rows = grid->num_rows; const int old_cols = grid->num_cols; - assert(old_rows != new_rows || old_cols != new_cols); + //assert(old_rows != new_rows || old_cols != new_cols); int new_col_idx = 0; int new_row_idx = 0; diff --git a/input.c b/input.c index d04b228d..efa58c9d 100644 --- a/input.c +++ b/input.c @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -15,11 +16,15 @@ #include #include +#include + #define LOG_MODULE "input" #define LOG_ENABLE_DBG 0 #include "log.h" +#include "config.h" #include "commands.h" #include "keymap.h" +#include "quirks.h" #include "render.h" #include "search.h" #include "selection.h" @@ -34,7 +39,6 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, char *map_str = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); - /* TODO: free old context + keymap */ if (wayl->kbd.xkb_compose_state != NULL) { xkb_compose_state_unref(wayl->kbd.xkb_compose_state); wayl->kbd.xkb_compose_state = NULL; @@ -86,12 +90,13 @@ keyboard_enter(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, assert(surface != NULL); struct wayland *wayl = data; + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; + + wayl->kbd_focus = term; wayl->input_serial = serial; - wayl->kbd_focus = wayl_terminal_from_surface(wayl, surface); - assert(wayl->kbd_focus != NULL); term_kbd_focus_in(wayl->kbd_focus); - term_xcursor_update(wayl->kbd_focus); } static bool @@ -145,7 +150,8 @@ keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, assert( wayl->kbd_focus == NULL || surface == NULL || /* Seen on Sway 1.2 */ - wayl_terminal_from_surface(wayl, surface) == wayl->kbd_focus); + ((const struct wl_window *)wl_surface_get_user_data(surface))->term == wayl->kbd_focus + ); struct terminal *old_focused = wayl->kbd_focus; wayl->kbd_focus = NULL; @@ -157,10 +163,9 @@ keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, wayl->kbd.meta = false;; xkb_compose_state_reset(wayl->kbd.xkb_compose_state); - if (old_focused != NULL) { + if (old_focused != NULL) term_kbd_focus_out(old_focused); - term_xcursor_update(old_focused); - } else { + else { /* * Sway bug - under certain conditions we get a * keyboard_leave() (and keyboard_key()) without first having @@ -580,7 +585,7 @@ keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, wayl->kbd.meta = xkb_state_mod_index_is_active( wayl->kbd.xkb_state, wayl->kbd.mod_meta, XKB_STATE_MODS_DEPRESSED); - if (wayl->kbd_focus) + if (wayl->kbd_focus && wayl->kbd_focus->active_surface == TERM_SURF_GRID) term_xcursor_update(wayl->kbd_focus); } @@ -609,6 +614,61 @@ input_repeat(struct wayland *wayl, uint32_t key) keyboard_key(wayl, NULL, wayl->input_serial, 0, key, XKB_KEY_DOWN); } +static bool +is_top_left(const struct terminal *term, int x, int y) +{ + int csd_border_size = term->conf->csd.border_width; + return ( + (term->active_surface == TERM_SURF_BORDER_LEFT && y < 10 * term->scale) || + (term->active_surface == TERM_SURF_BORDER_TOP && x < (10 + csd_border_size) * term->scale)); +} + +static bool +is_top_right(const struct terminal *term, int x, int y) +{ + int csd_border_size = term->conf->csd.border_width; + return ( + (term->active_surface == TERM_SURF_BORDER_RIGHT && y < 10 * term->scale) || + (term->active_surface == TERM_SURF_BORDER_TOP && x > term->width + 1 * csd_border_size * term->scale - 10 * term->scale)); +} + +static bool +is_bottom_left(const struct terminal *term, int x, int y) +{ + int csd_title_size = term->conf->csd.title_height; + int csd_border_size = term->conf->csd.border_width; + return ( + (term->active_surface == TERM_SURF_BORDER_LEFT && y > csd_title_size * term->scale + term->height) || + (term->active_surface == TERM_SURF_BORDER_BOTTOM && x < (10 + csd_border_size) * term->scale)); +} + +static bool +is_bottom_right(const struct terminal *term, int x, int y) +{ + int csd_title_size = term->conf->csd.title_height; + int csd_border_size = term->conf->csd.border_width; + return ( + (term->active_surface == TERM_SURF_BORDER_RIGHT && y > csd_title_size * term->scale + term->height) || + (term->active_surface == TERM_SURF_BORDER_BOTTOM && x > term->width + 1 * csd_border_size * term->scale - 10 * term->scale)); +} + +static const char * +xcursor_for_csd_border(struct terminal *term, int x, int y) +{ + if (is_top_left(term, x, y)) return "top_left_corner"; + else if (is_top_right(term, x, y)) return "top_right_corner"; + else if (is_bottom_left(term, x, y)) return "bottom_left_corner"; + else if (is_bottom_right(term, x, y)) return "bottom_right_corner"; + else if (term->active_surface == TERM_SURF_BORDER_LEFT) return "left_side"; + else if (term->active_surface == TERM_SURF_BORDER_RIGHT) return "right_side"; + else if (term->active_surface == TERM_SURF_BORDER_TOP) return "top_side"; + else if (term->active_surface == TERM_SURF_BORDER_BOTTOM) return"bottom_side"; + else { + assert(false); + return NULL; + } +} + static void wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, @@ -617,7 +677,8 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, assert(surface != NULL); struct wayland *wayl = data; - struct terminal *term = wayl_terminal_from_surface(wayl, surface); + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; LOG_DBG("pointer-enter: surface = %p, new-moused = %p", surface, term); @@ -626,10 +687,39 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, int x = wl_fixed_to_int(surface_x) * term->scale; int y = wl_fixed_to_int(surface_y) * term->scale; - wayl->mouse.col = x / term->cell_width; - wayl->mouse.row = y / term->cell_height; + switch ((term->active_surface = term_surface_kind(term, surface))) { + case TERM_SURF_GRID: + wayl->mouse.col = x / term->cell_width; + wayl->mouse.row = y / term->cell_height; + term_xcursor_update(term); + break; - term_xcursor_update(term); + case TERM_SURF_SEARCH: + case TERM_SURF_TITLE: + term->xcursor = "left_ptr"; + render_xcursor_set(term); + break; + + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + term->xcursor = xcursor_for_csd_border(term, x, y); + render_xcursor_set(term); + break; + + case TERM_SURF_BUTTON_MINIMIZE: + case TERM_SURF_BUTTON_MAXIMIZE: + case TERM_SURF_BUTTON_CLOSE: + term->xcursor = "left_ptr"; + render_xcursor_set(term); + render_refresh_csd(term); + break; + + case TERM_SURF_NONE: + assert(false); + break; + } } static void @@ -649,13 +739,49 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, wayl->pointer.xcursor = NULL; } + /* Reset mouse state */ + memset(&wayl->mouse, 0, sizeof(wayl->mouse)); + wayl->mouse_focus = NULL; if (old_moused == NULL) { LOG_WARN( "compositor sent pointer_leave event without a pointer_enter " "event: surface=%p", surface); - } else + } else { + if (surface != NULL) { + /* Sway 1.4 sends this event with a NULL surface when we destroy the window */ + const struct wl_window *win __attribute__((unused)) + = wl_surface_get_user_data(surface); + assert(old_moused == win->term); + } + + enum term_surface active_surface = old_moused->active_surface; + + old_moused->active_surface = TERM_SURF_NONE; term_xcursor_update(old_moused); + + switch (active_surface) { + case TERM_SURF_BUTTON_MINIMIZE: + case TERM_SURF_BUTTON_MAXIMIZE: + case TERM_SURF_BUTTON_CLOSE: + if (old_moused->is_shutting_down) + break; + + render_refresh_csd(old_moused); + break; + + case TERM_SURF_NONE: + case TERM_SURF_GRID: + case TERM_SURF_SEARCH: + case TERM_SURF_TITLE: + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + break; + } + + } } static void @@ -664,6 +790,7 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, { struct wayland *wayl = data; struct terminal *term = wayl->mouse_focus; + struct wl_window *win = term->window; /* Workaround buggy Sway 1.2 */ if (term == NULL) { @@ -687,29 +814,76 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, int x = wl_fixed_to_int(surface_x) * term->scale; int y = wl_fixed_to_int(surface_y) * term->scale; - int col = (x - term->margins.left) / term->cell_width; - int row = (y - term->margins.top) / term->cell_height; + wayl->mouse.x = x; + wayl->mouse.y = y; - if (col < 0 || row < 0 || col >= term->cols || row >= term->rows) - return; + switch (term->active_surface) { + case TERM_SURF_NONE: + case TERM_SURF_SEARCH: + case TERM_SURF_BUTTON_MINIMIZE: + case TERM_SURF_BUTTON_MAXIMIZE: + case TERM_SURF_BUTTON_CLOSE: + break; - bool update_selection = wayl->mouse.button == BTN_LEFT; - bool update_selection_early = term->selection.end.row == -1; + case TERM_SURF_TITLE: + /* We've started a 'move' timer, but user started dragging + * right away - abort the timer and initiate the actual move + * right away */ + if (wayl->mouse.button == BTN_LEFT && win->csd.move_timeout_fd != -1) { + fdm_del(wayl->fdm, win->csd.move_timeout_fd); + win->csd.move_timeout_fd = -1; + xdg_toplevel_move(win->xdg_toplevel, wayl->seat, win->csd.serial); + } + break; - if (update_selection && update_selection_early) - selection_update(term, col, row); + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + term->xcursor = xcursor_for_csd_border(term, x, y); + render_xcursor_set(term); + break; - if (col == wayl->mouse.col && row == wayl->mouse.row) - return; + case TERM_SURF_GRID: { + int col = (x - term->margins.left) / term->cell_width; + int row = (y - term->margins.top) / term->cell_height; - wayl->mouse.col = col; - wayl->mouse.row = row; + if (col < 0 || row < 0 || col >= term->cols || row >= term->rows) + return; - if (update_selection && !update_selection_early) - selection_update(term, col, row); + bool update_selection = wayl->mouse.button == BTN_LEFT; + bool update_selection_early = term->selection.end.row == -1; - term_mouse_motion( + if (update_selection && update_selection_early) + selection_update(term, col, row); + + if (col == wayl->mouse.col && row == wayl->mouse.row) + break; + + wayl->mouse.col = col; + wayl->mouse.row = row; + + if (update_selection && !update_selection_early) + selection_update(term, col, row); + + term_mouse_motion( term, wayl->mouse.button, wayl->mouse.row, wayl->mouse.col); + break; + } + } +} + +static bool +fdm_csd_move(struct fdm *fdm, int fd, int events, void *data) +{ + struct wl_window *win = data; + struct wayland *wayl = win->term->wl; + + fdm_del(fdm, fd); + win->csd.move_timeout_fd = -1; + + xdg_toplevel_move(win->xdg_toplevel, wayl->seat, win->csd.serial); + return true; } static void @@ -739,10 +913,9 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, } assert(term != NULL); - search_cancel(term); - switch (state) { - case WL_POINTER_BUTTON_STATE_PRESSED: { + /* Update double/triple click state */ + if (state == WL_POINTER_BUTTON_STATE_PRESSED) { /* Time since last click */ struct timeval now, since_last; gettimeofday(&now, NULL); @@ -757,45 +930,161 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, } else wayl->mouse.count = 1; - if (button == BTN_LEFT) { - switch (wayl->mouse.count) { - case 1: - selection_start( - term, wayl->mouse.col, wayl->mouse.row, - wayl->kbd.ctrl ? SELECTION_BLOCK : SELECTION_NORMAL); - break; - - case 2: - selection_mark_word(term, wayl->mouse.col, wayl->mouse.row, - wayl->kbd.ctrl, serial); - break; - - case 3: - selection_mark_row(term, wayl->mouse.row, serial); - break; - } - } else { - if (wayl->mouse.count == 1 && button == BTN_MIDDLE && selection_enabled(term)) - selection_from_primary(term); - selection_cancel(term); - } - wayl->mouse.button = button; /* For motion events */ wayl->mouse.last_button = button; wayl->mouse.last_time = now; - term_mouse_down(term, button, wayl->mouse.row, wayl->mouse.col); + } else + wayl->mouse.button = 0; /* For motion events */ + + switch (term->active_surface) { + case TERM_SURF_TITLE: + if (state == WL_POINTER_BUTTON_STATE_PRESSED) { + + struct wl_window *win = term->window; + + /* Toggle maximized state on double-click */ + if (button == BTN_LEFT && wayl->mouse.count == 2) { + if (win->is_maximized) + xdg_toplevel_unset_maximized(win->xdg_toplevel); + else + xdg_toplevel_set_maximized(win->xdg_toplevel); + } + + else if (button == BTN_LEFT && win->csd.move_timeout_fd == -1) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 200000000}, + }; + + int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd >= 0 && + timerfd_settime(fd, 0, &timeout, NULL) == 0 && + fdm_add(wayl->fdm, fd, EPOLLIN, &fdm_csd_move, win)) + { + win->csd.move_timeout_fd = fd; + win->csd.serial = serial; + } else { + LOG_ERRNO("failed to configure XDG toplevel move timer FD"); + close(fd); + } + } + } + + else if (state == WL_POINTER_BUTTON_STATE_RELEASED) { + struct wl_window *win = term->window; + if (win->csd.move_timeout_fd != -1) { + fdm_del(wayl->fdm, win->csd.move_timeout_fd); + win->csd.move_timeout_fd = -1; + } + } + return; + + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: { + static const enum xdg_toplevel_resize_edge map[] = { + [TERM_SURF_BORDER_LEFT] = XDG_TOPLEVEL_RESIZE_EDGE_LEFT, + [TERM_SURF_BORDER_RIGHT] = XDG_TOPLEVEL_RESIZE_EDGE_RIGHT, + [TERM_SURF_BORDER_TOP] = XDG_TOPLEVEL_RESIZE_EDGE_TOP, + [TERM_SURF_BORDER_BOTTOM] = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM, + }; + + if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) { + enum xdg_toplevel_resize_edge resize_type; + + int x = wayl->mouse.x; + int y = wayl->mouse.y; + + if (is_top_left(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT; + else if (is_top_right(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT; + else if (is_bottom_left(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT; + else if (is_bottom_right(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT; + else + resize_type = map[term->active_surface]; + + xdg_toplevel_resize( + term->window->xdg_toplevel, term->wl->seat, serial, resize_type); + } + return; + } + + case TERM_SURF_BUTTON_MINIMIZE: + if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) + xdg_toplevel_set_minimized(term->window->xdg_toplevel); + break; + + case TERM_SURF_BUTTON_MAXIMIZE: + if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) { + if (term->window->is_maximized) + xdg_toplevel_unset_maximized(term->window->xdg_toplevel); + else + xdg_toplevel_set_maximized(term->window->xdg_toplevel); + } + break; + + case TERM_SURF_BUTTON_CLOSE: + if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) + term_shutdown(term); + break; + + case TERM_SURF_SEARCH: + break; + + case TERM_SURF_GRID: { + search_cancel(term); + + switch (state) { + case WL_POINTER_BUTTON_STATE_PRESSED: { + if (button == BTN_LEFT) { + switch (wayl->mouse.count) { + case 1: + selection_start( + term, wayl->mouse.col, wayl->mouse.row, + wayl->kbd.ctrl ? SELECTION_BLOCK : SELECTION_NORMAL); + break; + + case 2: + selection_mark_word(term, wayl->mouse.col, wayl->mouse.row, + wayl->kbd.ctrl, serial); + break; + + case 3: + selection_mark_row(term, wayl->mouse.row, serial); + break; + } + } else { + if (wayl->mouse.count == 1 && button == BTN_MIDDLE && + selection_enabled(term)) + { + selection_from_primary(term); + } + selection_cancel(term); + } + + term_mouse_down(term, button, wayl->mouse.row, wayl->mouse.col); + break; + } + + case WL_POINTER_BUTTON_STATE_RELEASED: + if (button != BTN_LEFT || term->selection.end.col == -1) + selection_cancel(term); + else + selection_finalize(term, serial); + + term_mouse_up(term, button, wayl->mouse.row, wayl->mouse.col); + break; + } break; } - case WL_POINTER_BUTTON_STATE_RELEASED: - if (button != BTN_LEFT || term->selection.end.col == -1) - selection_cancel(term); - else - selection_finalize(term, serial); - - wayl->mouse.button = 0; /* For motion events */ - term_mouse_up(term, button, wayl->mouse.row, wayl->mouse.col); + case TERM_SURF_NONE: + assert(false); break; + } } diff --git a/meson.build b/meson.build index 21c23718..fcb85f90 100644 --- a/meson.build +++ b/meson.build @@ -112,6 +112,7 @@ executable( 'main.c', 'misc.c', 'misc.h', 'osc.c', 'osc.h', + 'quirks.c', 'quirks.h', 'render.c', 'render.h', 'search.c', 'search.h', 'selection.c', 'selection.h', diff --git a/quirks.c b/quirks.c new file mode 100644 index 00000000..6db96e46 --- /dev/null +++ b/quirks.c @@ -0,0 +1,77 @@ +#include "quirks.h" + +#include +#include + +#define LOG_MODULE "quirks" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#define ALEN(v) (sizeof(v) / sizeof(v[0])) + +static bool +is_weston(void) +{ + /* + * On weston (8.0), synchronized subsurfaces aren't updated + * correctly. + + * They appear to render once, but after that, updates are + * sporadic. Sometimes they update, most of the time they + * don't. + * + * Adding explicit parent surface commits right after the + * subsurface commit doesn't help (and would be useless anyway, + * since it would defeat the purpose of having the subsurface + * synchronized in the first place). + */ + static bool is_weston = false; + static bool initialized = false; + + if (!initialized) { + initialized = true; + is_weston = getenv("WESTON_CONFIG_FILE") != NULL; + if (is_weston) + LOG_WARN("applying wl_subsurface_set_desync() workaround for weston"); + } + + return is_weston; +} + +void +quirk_weston_subsurface_desync_on(struct wl_subsurface *sub) +{ + if (!is_weston()) + return; + + wl_subsurface_set_desync(sub); +} + +void +quirk_weston_subsurface_desync_off(struct wl_subsurface *sub) +{ + if (!is_weston()) + return; + + wl_subsurface_set_sync(sub); +} + +void +quirk_weston_csd_on(struct terminal *term) +{ + if (term->window->use_csd != CSD_YES) + return; + + for (int i = 0; i < ALEN(term->window->csd.surface); i++) + quirk_weston_subsurface_desync_on(term->window->csd.sub_surface[i]); +} + +void +quirk_weston_csd_off(struct terminal *term) +{ + if (term->window->use_csd != CSD_YES) + return; + + for (int i = 0; i < ALEN(term->window->csd.surface); i++) + quirk_weston_subsurface_desync_off(term->window->csd.sub_surface[i]); +} diff --git a/quirks.h b/quirks.h new file mode 100644 index 00000000..7b6ab4f8 --- /dev/null +++ b/quirks.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include "terminal.h" + +void quirk_weston_subsurface_desync_on(struct wl_subsurface *sub); +void quirk_weston_subsurface_desync_off(struct wl_subsurface *sub); + +/* Shortcuts to call desync_{on,off} on all CSD subsurfaces */ +void quirk_weston_csd_on(struct terminal *term); +void quirk_weston_csd_off(struct terminal *term); diff --git a/render.c b/render.c index 179ceb1e..fdb156ea 100644 --- a/render.c +++ b/render.c @@ -18,6 +18,7 @@ #include "log.h" #include "config.h" #include "grid.h" +#include "quirks.h" #include "selection.h" #include "shm.h" @@ -38,6 +39,10 @@ static struct { static void fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data); +#define shm_cookie_grid(term) ((unsigned long)((uintptr_t)term + 0)) +#define shm_cookie_search(term) ((unsigned long)((uintptr_t)term + 1)) +#define shm_cookie_csd(term, n) ((unsigned long)((uintptr_t)term + 2 + (n))) /* Should be placed last */ + struct renderer * render_init(struct fdm *fdm, struct wayland *wayl) { @@ -211,6 +216,9 @@ color_hex_to_rgb(uint32_t color) static inline pixman_color_t color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha) { + if (alpha == 0) + return (pixman_color_t){0, 0, 0, 0}; + int alpha_div = 0xffff / alpha; return (pixman_color_t){ .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)) / alpha_div, @@ -655,6 +663,389 @@ render_worker_thread(void *_ctx) return -1; } +struct csd_data { + int x; + int y; + int width; + int height; +}; + +static struct csd_data +get_csd_data(const struct terminal *term, enum csd_surface surf_idx) +{ + assert(term->window->use_csd == CSD_YES); + + /* Only title bar is rendered in maximized mode */ + const int border_width = !term->window->is_maximized + ? term->conf->csd.border_width * term->scale : 0; + + const int title_height = !term->window->is_fullscreen + ? term->conf->csd.title_height * term->scale : 0; + + const int button_width = !term->window->is_fullscreen + ? term->conf->csd.button_width * term->scale : 0; + + switch (surf_idx) { + case CSD_SURF_TITLE: return (struct csd_data){ 0, -title_height, term->width, title_height}; + case CSD_SURF_LEFT: return (struct csd_data){-border_width, -title_height, border_width, title_height + term->height}; + case CSD_SURF_RIGHT: return (struct csd_data){ term->width, -title_height, border_width, title_height + term->height}; + case CSD_SURF_TOP: return (struct csd_data){-border_width, -title_height - border_width, term->width + 2 * border_width, border_width}; + case CSD_SURF_BOTTOM: return (struct csd_data){-border_width, term->height, term->width + 2 * border_width, border_width}; + + /* Positioned relative to CSD_SURF_TITLE */ + case CSD_SURF_MINIMIZE: return (struct csd_data){term->width - 3 * button_width, 0, button_width, title_height}; + case CSD_SURF_MAXIMIZE: return (struct csd_data){term->width - 2 * button_width, 0, button_width, title_height}; + case CSD_SURF_CLOSE: return (struct csd_data){term->width - 1 * button_width, 0, button_width, title_height}; + + case CSD_SURF_COUNT: + assert(false); + return (struct csd_data){0}; + } + + assert(false); + return (struct csd_data){0}; +} + +static void +csd_commit(struct terminal *term, struct wl_surface *surf, struct buffer *buf) +{ + wl_surface_attach(surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(surf, 0, 0, buf->width, buf->height); + wl_surface_set_buffer_scale(surf, term->scale); + wl_surface_commit(surf); +} + +static void +render_csd_part(struct terminal *term, + struct wl_surface *surf, struct buffer *buf, + int width, int height, pixman_color_t *color) +{ + assert(term->window->use_csd == CSD_YES); + + pixman_image_t *src = pixman_image_create_solid_fill(color); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix, color, 1, + &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); + pixman_image_unref(src); +} + +static void +render_csd_title(struct terminal *term) +{ + assert(term->window->use_csd == CSD_YES); + + struct csd_data info = get_csd_data(term, CSD_SURF_TITLE); + struct wl_surface *surf = term->window->csd.surface[CSD_SURF_TITLE]; + + assert(info.width > 0 && info.height > 0); + + unsigned long cookie = shm_cookie_csd(term, CSD_SURF_TITLE); + struct buffer *buf = shm_get_buffer( + term->wl->shm, info.width, info.height, cookie); + + uint32_t _color = term->colors.default_fg; + uint16_t alpha = 0xffff; + + if (term->conf->csd.color.title_set) { + _color = term->conf->csd.color.title; + alpha = _color >> 24 | (_color >> 24 << 8); + } + + pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha); + if (!term->visual_focus) + pixman_color_dim(&color); + render_csd_part(term, surf, buf, info.width, info.height, &color); + csd_commit(term, surf, buf); +} + +static void +render_csd_border(struct terminal *term, enum csd_surface surf_idx) +{ + assert(term->window->use_csd == CSD_YES); + assert(surf_idx >= CSD_SURF_LEFT && surf_idx <= CSD_SURF_BOTTOM); + + struct csd_data info = get_csd_data(term, surf_idx); + struct wl_surface *surf = term->window->csd.surface[surf_idx]; + + if (info.width == 0 || info.height == 0) + return; + + unsigned long cookie = shm_cookie_csd(term, surf_idx); + struct buffer *buf = shm_get_buffer( + term->wl->shm, info.width, info.height, cookie); + + pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0); + render_csd_part(term, surf, buf, info.width, info.height, &color); + csd_commit(term, surf, buf); +} + +static void +render_csd_button_minimize(struct terminal *term, struct buffer *buf) +{ + pixman_color_t color = color_hex_to_pixman(term->colors.default_bg); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 2; + const int max_width = buf->width / 2; + + int width = max_width; + int height = max_width / 2; + + if (height > max_height) { + height = max_height; + width = height * 2; + } + + assert(width <= max_width); + assert(height <= max_height); + + int x_margin = (buf->width - width) / 2.; + int y_margin = (buf->height - height) / 2.; + + pixman_triangle_t tri = { + .p1 = { + .x = pixman_int_to_fixed(x_margin), + .y = pixman_int_to_fixed(y_margin), + }, + .p2 = { + .x = pixman_int_to_fixed(x_margin + width), + .y = pixman_int_to_fixed(y_margin), + }, + .p3 = { + .x = pixman_int_to_fixed(buf->width / 2), + .y = pixman_int_to_fixed(y_margin + height), + }, + }; + + pixman_composite_triangles( + PIXMAN_OP_OVER, src, buf->pix, PIXMAN_a1, + 0, 0, 0, 0, 1, &tri); + pixman_image_unref(src); +} + +static void +render_csd_button_maximize_maximized( + struct terminal *term, struct buffer *buf) +{ + pixman_color_t color = color_hex_to_pixman(term->colors.default_bg); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; + + int width = min(max_height, max_width); + int thick = 1 * term->scale; + + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix, &color, 4, + (pixman_rectangle16_t[]){ + {x_margin, y_margin, width, thick}, + {x_margin, y_margin + thick, thick, width - 2 * thick}, + {x_margin + width - thick, y_margin + thick, thick, width - 2 * thick}, + {x_margin, y_margin + width - thick, width, thick}}); + + pixman_image_unref(src); + +} + +static void +render_csd_button_maximize_window( + struct terminal *term, struct buffer *buf) +{ + pixman_color_t color = color_hex_to_pixman(term->colors.default_bg); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 2; + const int max_width = buf->width / 2; + + int width = max_width; + int height = max_width / 2; + + if (height > max_height) { + height = max_height; + width = height * 2; + } + + assert(width <= max_width); + assert(height <= max_height); + + int x_margin = (buf->width - width) / 2.; + int y_margin = (buf->height - height) / 2.; + + pixman_triangle_t tri = { + .p1 = { + .x = pixman_int_to_fixed(buf->width / 2), + .y = pixman_int_to_fixed(y_margin), + }, + .p2 = { + .x = pixman_int_to_fixed(x_margin), + .y = pixman_int_to_fixed(y_margin + height), + }, + .p3 = { + .x = pixman_int_to_fixed(x_margin + width), + .y = pixman_int_to_fixed(y_margin + height), + }, + }; + + pixman_composite_triangles( + PIXMAN_OP_OVER, src, buf->pix, PIXMAN_a1, + 0, 0, 0, 0, 1, &tri); + + pixman_image_unref(src); +} + +static void +render_csd_button_maximize(struct terminal *term, struct buffer *buf) +{ + if (term->window->is_maximized) + render_csd_button_maximize_maximized(term, buf); + else + render_csd_button_maximize_window(term, buf); +} + +static void +render_csd_button_close(struct terminal *term, struct buffer *buf) +{ + pixman_color_t color = color_hex_to_pixman(term->colors.default_bg); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; + + int width = min(max_height, max_width); + + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix, &color, 1, + &(pixman_rectangle16_t){x_margin, y_margin, width, width}); + + pixman_image_unref(src); +} + +static void +render_csd_button(struct terminal *term, enum csd_surface surf_idx) +{ + assert(term->window->use_csd == CSD_YES); + assert(surf_idx >= CSD_SURF_MINIMIZE && surf_idx <= CSD_SURF_CLOSE); + + struct csd_data info = get_csd_data(term, surf_idx); + struct wl_surface *surf = term->window->csd.surface[surf_idx]; + + if (info.width == 0 || info.height == 0) + return; + + unsigned long cookie = shm_cookie_csd(term, surf_idx); + struct buffer *buf = shm_get_buffer( + term->wl->shm, info.width, info.height, cookie); + + uint32_t _color; + uint16_t alpha = 0xffff; + bool is_active = false; + const bool *is_set = NULL; + const uint32_t *conf_color = NULL; + + switch (surf_idx) { + case CSD_SURF_MINIMIZE: + _color = 0xff1e90ff; + is_set = &term->conf->csd.color.minimize_set; + conf_color = &term->conf->csd.color.minimize; + is_active = term->active_surface == TERM_SURF_BUTTON_MINIMIZE; + break; + + case CSD_SURF_MAXIMIZE: + _color = 0xff30ff30; + is_set = &term->conf->csd.color.maximize_set; + conf_color = &term->conf->csd.color.maximize; + is_active = term->active_surface == TERM_SURF_BUTTON_MAXIMIZE; + break; + + case CSD_SURF_CLOSE: + _color = 0xffff3030; + is_set = &term->conf->csd.color.close_set; + conf_color = &term->conf->csd.color.close; + is_active = term->active_surface == TERM_SURF_BUTTON_CLOSE; + break; + + default: + assert(false); + break; + } + + if (is_active) { + if (*is_set) { + _color = *conf_color; + alpha = _color >> 24 | (_color >> 24 << 8); + } + } else { + _color = 0; + alpha = 0; + } + + pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha); + if (!term->visual_focus) + pixman_color_dim(&color); + render_csd_part(term, surf, buf, info.width, info.height, &color); + + switch (surf_idx) { + case CSD_SURF_MINIMIZE: render_csd_button_minimize(term, buf); break; + case CSD_SURF_MAXIMIZE: render_csd_button_maximize(term, buf); break; + case CSD_SURF_CLOSE: render_csd_button_close(term, buf); break; + break; + + default: + assert(false); + break; + } + + csd_commit(term, surf, buf); +} + +static void +render_csd(struct terminal *term) +{ + assert(term->window->use_csd == CSD_YES); + + if (term->window->is_fullscreen) + return; + + for (size_t i = 0; i < CSD_SURF_COUNT; i++) { + struct csd_data info = get_csd_data(term, i); + const int x = info.x; + const int y = info.y; + const int width = info.width; + const int height = info.height; + + struct wl_surface *surf = term->window->csd.surface[i]; + struct wl_subsurface *sub = term->window->csd.sub_surface[i]; + + assert(surf != NULL); + assert(sub != NULL); + + if (width == 0 || height == 0) { + /* CSD borders aren't rendered in maximized mode */ + assert(term->window->is_maximized || term->window->is_fullscreen); + wl_subsurface_set_position(sub, 0, 0); + wl_surface_attach(surf, NULL, 0, 0); + wl_surface_commit(surf); + continue; + } + + wl_subsurface_set_position(sub, x / term->scale, y / term->scale); + } + + for (size_t i = CSD_SURF_LEFT; i <= CSD_SURF_BOTTOM; i++) + render_csd_border(term, i); + for (size_t i = CSD_SURF_MINIMIZE; i <= CSD_SURF_CLOSE; i++) + render_csd_button(term, i); + render_csd_title(term); +} + static void frame_callback( void *data, struct wl_callback *wl_callback, uint32_t callback_data); @@ -678,13 +1069,16 @@ grid_render(struct terminal *term) assert(term->width > 0); assert(term->height > 0); - unsigned long cookie = (uintptr_t)term; + unsigned long cookie = shm_cookie_grid(term); struct buffer *buf = shm_get_buffer( term->wl->shm, term->width, term->height, cookie); wl_surface_attach(term->window->surface, buf->wl_buf, 0, 0); pixman_image_t *pix = buf->pix; + pixman_region16_t clip; + pixman_region_init_rect(&clip, term->margins.left, term->margins.top, term->cols * term->cell_width, term->rows * term->cell_height); + pixman_image_set_clip_region(pix, &clip); /* If we resized the window, or is flashing, or just stopped flashing */ if (term->render.last_buf != buf || @@ -718,6 +1112,7 @@ grid_render(struct terminal *term) if (term->is_searching) pixman_color_dim(&bg); + pixman_image_set_clip_region(pix, NULL); pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, &bg, 4, (pixman_rectangle16_t[]){ @@ -725,6 +1120,7 @@ grid_render(struct terminal *term) {0, 0, term->margins.left, term->height}, /* Left */ {rmargin, 0, term->margins.right, term->height}, /* Right */ {0, bmargin, term->width, term->margins.bottom}}); /* Bottom */ + pixman_image_set_clip_region(pix, &clip); wl_surface_damage_buffer( term->window->surface, 0, 0, term->width, term->margins.top); @@ -954,41 +1350,29 @@ grid_render(struct terminal *term) } static void -frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) -{ - struct terminal *term = data; - - assert(term->window->frame_callback == wl_callback); - wl_callback_destroy(wl_callback); - term->window->frame_callback = NULL; - - if (term->render.pending) { - term->render.pending = false; - grid_render(term); - } -} - -void render_search_box(struct terminal *term) { assert(term->window->search_sub_surface != NULL); const size_t wanted_visible_chars = max(20, term->search.len); - const int scale = term->scale >= 1 ? term->scale : 1; - const size_t margin = scale * 3; + assert(term->scale >= 1); + const int scale = term->scale; - const size_t width = min( + const size_t margin = 3 * scale; + + const size_t width = term->width - 2 * margin; + const size_t visible_width = min( term->width - 2 * margin, 2 * margin + wanted_visible_chars * term->cell_width); const size_t height = min( term->height - 2 * margin, 2 * margin + 1 * term->cell_height); - const size_t visible_chars = (width - 2 * margin) / term->cell_width; + const size_t visible_chars = (visible_width - 2 * margin) / term->cell_width; size_t glyph_offset = term->render.search_glyph_offset; - unsigned long cookie = (uintptr_t)term + 1; + unsigned long cookie = shm_cookie_search(term); struct buffer *buf = shm_get_buffer(term->wl->shm, width, height, cookie); /* Background - yellow on empty/match, red on mismatch */ @@ -998,15 +1382,20 @@ render_search_box(struct terminal *term) pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix, &color, - 1, &(pixman_rectangle16_t){0, 0, width, height}); + 1, &(pixman_rectangle16_t){width - visible_width, 0, visible_width, height}); + + pixman_color_t transparent = color_hex_to_pixman_with_alpha(0, 0); + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix, &transparent, + 1, &(pixman_rectangle16_t){0, 0, width - visible_width, height}); struct font *font = term->fonts[0]; - int x = margin; + int x = width - visible_width + margin; int y = margin; pixman_color_t fg = color_hex_to_pixman(term->colors.table[0]); if (term->search.cursor < glyph_offset || - term->search.cursor >= glyph_offset + visible_chars + 2) + term->search.cursor >= glyph_offset + visible_chars + 1) { /* Make sure cursor is always visible */ term->render.search_glyph_offset = glyph_offset = term->search.cursor; @@ -1014,7 +1403,7 @@ render_search_box(struct terminal *term) /* Text (what the user entered - *not* match(es)) */ for (size_t i = glyph_offset; - i < term->search.len && i - glyph_offset < visible_chars + 1; + i < term->search.len && i - glyph_offset < visible_chars; i++) { if (i == term->search.cursor) @@ -1037,23 +1426,70 @@ render_search_box(struct terminal *term) if (term->search.cursor >= term->search.len) draw_bar(term, buf->pix, font, &fg, x, y); + quirk_weston_subsurface_desync_on(term->window->search_sub_surface); + + /* TODO: this is only necessary on a window resize */ wl_subsurface_set_position( term->window->search_sub_surface, - max(0, (int32_t)term->width - width - margin), - max(0, (int32_t)term->height - height - margin)); + margin / scale, + max(0, (int32_t)term->height - height - margin) / scale); - wl_surface_damage_buffer(term->window->search_surface, 0, 0, width, height); wl_surface_attach(term->window->search_surface, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(term->window->search_surface, 0, 0, width, height); wl_surface_set_buffer_scale(term->window->search_surface, scale); + + struct wl_region *region = wl_compositor_create_region(term->wl->compositor); + if (region != NULL) { + wl_region_add(region, width - visible_width, 0, visible_width, height); + wl_surface_set_opaque_region(term->window->search_surface, region); + wl_region_destroy(region); + } + wl_surface_commit(term->window->search_surface); + quirk_weston_subsurface_desync_off(term->window->search_sub_surface); +} + +static void +frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) +{ + struct terminal *term = data; + + assert(term->window->frame_callback == wl_callback); + wl_callback_destroy(wl_callback); + term->window->frame_callback = NULL; + + if (term->render.pending.csd) { + term->render.pending.csd = false; + + if (term->window->use_csd == CSD_YES) { + quirk_weston_csd_on(term); + render_csd(term); + quirk_weston_csd_off(term); + } + } + + if (term->render.pending.search) { + term->render.pending.search = false; + + if (term->is_searching) + render_search_box(term); + } + + if (term->render.pending.grid) { + term->render.pending.grid = false; + grid_render(term); + } } /* Move to terminal.c? */ -static void +static bool maybe_resize(struct terminal *term, int width, int height, bool force) { - if (!force && (width == 0 || height == 0)) - return; + if (!term->window->is_configured) + return false; + + if (term->cell_width == 0 && term->cell_height == 0) + return false; int scale = -1; tll_foreach(term->window->on_outputs, it) { @@ -1069,13 +1505,50 @@ maybe_resize(struct terminal *term, int width, int height, bool force) width *= scale; height *= scale; - if (!force && width == 0 && height == 0) { - /* Assume we're not fully up and running yet */ - return; + if (width == 0 && height == 0) { + /* + * The compositor is letting us choose the size + * + * If we have a "last" used size - use that. Otherwise, use + * the size from the user configuration. + */ + if (term->unmaximized_width != 0 && term->unmaximized_height != 0) { + width = term->unmaximized_width; + height = term->unmaximized_height; + } else { + width = term->conf->width; + height = term->conf->height; + + if (term->window->use_csd == CSD_YES) { + /* Take CSD title bar into account */ + assert(!term->window->is_fullscreen); + height -= term->conf->csd.title_height; + } + + width *= scale; + height *= scale; + } } + /* Don't shrink grid too much */ + const int min_cols = 20; + const int min_rows = 4; + + /* Minimum window size */ + const int min_width = min_cols * term->cell_width; + const int min_height = min_rows * term->cell_height; + + width = max(width, min_width); + height = max(height, min_height); + + /* Padding */ + const int max_pad_x = (width - min_width) / 2; + const int max_pad_y = (height - min_height) / 2; + const int pad_x = min(max_pad_x, scale * term->conf->pad_x); + const int pad_y = min(max_pad_y, scale * term->conf->pad_y); + if (!force && width == term->width && height == term->height && scale == term->scale) - return; + return false; selection_cancel(term); @@ -1092,13 +1565,9 @@ maybe_resize(struct terminal *term, int width, int height, bool force) const int old_cols = term->cols; const int old_rows = term->rows; - /* Padding */ - const int pad_x = term->width > 2 * scale * term->conf->pad_x ? scale * term->conf->pad_x : 0; - const int pad_y = term->height > 2 * scale * term->conf->pad_y ? scale * term->conf->pad_y : 0; - /* Screen rows/cols after resize */ - const int new_cols = max((term->width - 2 * pad_x) / term->cell_width, 1); - const int new_rows = max((term->height - 2 * pad_y) / term->cell_height, 1); + const int new_cols = (term->width - 2 * pad_x) / term->cell_width; + const int new_rows = (term->height - 2 * pad_y) / term->cell_height; /* Grid rows/cols after resize */ const int new_normal_grid_rows = 1 << (32 - __builtin_clz(new_rows + scrollback_lines - 1)); @@ -1108,11 +1577,16 @@ maybe_resize(struct terminal *term, int width, int height, bool force) assert(new_rows >= 1); /* Margins */ - term->margins.left = (term->width - new_cols * term->cell_width) / 2; - term->margins.top = (term->height - new_rows * term->cell_height) / 2; + term->margins.left = pad_x; + term->margins.top = pad_y; term->margins.right = term->width - new_cols * term->cell_width - term->margins.left; term->margins.bottom = term->height - new_rows * term->cell_height - term->margins.top; + assert(term->margins.left >= pad_x); + assert(term->margins.right >= pad_x); + assert(term->margins.top >= pad_y); + assert(term->margins.bottom >= pad_y); + if (new_cols == old_cols && new_rows == old_rows) { LOG_DBG("grid layout unaffected; skipping reflow"); goto damage_view; @@ -1132,10 +1606,10 @@ maybe_resize(struct terminal *term, int width, int height, bool force) term->cols = new_cols; term->rows = new_rows; - LOG_INFO("resize: %dx%d, grid: cols=%d, rows=%d " - "(left-margin=%d, right-margin=%d, top-margin=%d, bottom-margin=%d)", - term->width, term->height, term->cols, term->rows, - term->margins.left, term->margins.right, term->margins.top, term->margins.bottom); + LOG_DBG("resize: %dx%d, grid: cols=%d, rows=%d " + "(left-margin=%d, right-margin=%d, top-margin=%d, bottom-margin=%d)", + term->width, term->height, term->cols, term->rows, + term->margins.left, term->margins.right, term->margins.top, term->margins.bottom); /* Signal TIOCSWINSZ */ if (ioctl(term->ptmx, TIOCSWINSZ, @@ -1174,20 +1648,49 @@ maybe_resize(struct terminal *term, int width, int height, bool force) term->render.last_cursor.cell = NULL; damage_view: + if (!term->window->is_maximized && !term->window->is_fullscreen) { + term->unmaximized_width = term->width; + term->unmaximized_height = term->height; + } + +#if 0 + /* TODO: doesn't include CSD title bar */ + xdg_toplevel_set_min_size( + term->window->xdg_toplevel, min_width / scale, min_height / scale); +#endif + + { + bool title_shown = !term->window->is_fullscreen && + term->window->use_csd == CSD_YES; + + int title_height = title_shown ? term->conf->csd.title_height : 0; + xdg_surface_set_window_geometry( + term->window->xdg_surface, + 0, + -title_height, + term->width / term->scale, + term->height / term->scale + title_height); + } + tll_free(term->normal.scroll_damage); tll_free(term->alt.scroll_damage); + term->render.last_buf = NULL; term_damage_view(term); + render_refresh_csd(term); + render_refresh_search(term); render_refresh(term); + + return true; } -void +bool render_resize(struct terminal *term, int width, int height) { return maybe_resize(term, width, height, false); } -void +bool render_resize_force(struct terminal *term, int width, int height) { return maybe_resize(term, width, height, true); @@ -1263,20 +1766,50 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) tll_foreach(renderer->wayl->terms, it) { struct terminal *term = it->item; - if (!term->render.refresh_needed) + if (!term->render.refresh.grid && + !term->render.refresh.csd && + !term->render.refresh.search) + { continue; + } - if (term->render.app_sync_updates.enabled) + if (term->render.app_sync_updates.enabled && + !term->render.refresh.csd && + !term->render.refresh.search) + { continue; + } + + if (term->render.refresh.csd || term->render.refresh.search) { + /* Force update of parent surface */ + term->render.refresh.grid = true; + } assert(term->window->is_configured); - term->render.refresh_needed = false; - if (term->window->frame_callback == NULL) - grid_render(term); - else { + bool grid = term->render.refresh.grid; + bool csd = term->render.refresh.csd; + bool search = term->render.refresh.search; + + term->render.refresh.grid = false; + term->render.refresh.csd = false; + term->render.refresh.search = false; + + if (term->window->frame_callback == NULL) { + if (csd && term->window->use_csd == CSD_YES) { + quirk_weston_csd_on(term); + render_csd(term); + quirk_weston_csd_off(term); + } + if (search) + render_search_box(term); + if (grid) + grid_render(term); + } else { /* Tells the frame callback to render again */ - term->render.pending = true; + term->render.pending.grid = grid; + term->render.pending.csd = csd; + term->render.pending.search = search; } } @@ -1311,7 +1844,21 @@ render_set_title(struct terminal *term, const char *_title) void render_refresh(struct terminal *term) { - term->render.refresh_needed = true; + term->render.refresh.grid = true; +} + +void +render_refresh_csd(struct terminal *term) +{ + if (term->window->use_csd == CSD_YES) + term->render.refresh.csd = true; +} + +void +render_refresh_search(struct terminal *term) +{ + if (term->is_searching) + term->render.refresh.search = true; } bool diff --git a/render.h b/render.h index 3bd52fa5..99246f17 100644 --- a/render.h +++ b/render.h @@ -1,4 +1,5 @@ #pragma once +#include #include "terminal.h" #include "fdm.h" @@ -8,15 +9,15 @@ struct renderer; struct renderer *render_init(struct fdm *fdm, struct wayland *wayl); void render_destroy(struct renderer *renderer); -void render_resize(struct terminal *term, int width, int height); -void render_resize_force(struct terminal *term, int width, int height); +bool render_resize(struct terminal *term, int width, int height); +bool render_resize_force(struct terminal *term, int width, int height); void render_set_title(struct terminal *term, const char *title); void render_refresh(struct terminal *term); +void render_refresh_csd(struct terminal *term); +void render_refresh_search(struct terminal *term); bool render_xcursor_set(struct terminal *term); -void render_search_box(struct terminal *term); - struct render_worker_context { int my_id; struct terminal *term; diff --git a/search.c b/search.c index a39c75a8..2c58f3f2 100644 --- a/search.c +++ b/search.c @@ -40,8 +40,14 @@ search_ensure_size(struct terminal *term, size_t wanted_size) static void search_cancel_keep_selection(struct terminal *term) { - wl_surface_attach(term->window->search_surface, NULL, 0, 0); - wl_surface_commit(term->window->search_surface); + struct wl_window *win = term->window; + if (win->search_sub_surface != NULL) + wl_subsurface_destroy(win->search_sub_surface); + if (win->search_surface != NULL) + wl_surface_destroy(win->search_surface); + + win->search_surface = NULL; + win->search_sub_surface = NULL; free(term->search.buf); term->search.buf = NULL; @@ -65,13 +71,22 @@ search_begin(struct terminal *term) search_cancel_keep_selection(term); selection_cancel(term); + /* On-demand instantiate wayland surface */ + struct wl_window *win = term->window; + struct wayland *wayl = term->wl; + win->search_surface = wl_compositor_create_surface(wayl->compositor); + wl_surface_set_user_data(win->search_surface, term->window); + + win->search_sub_surface = wl_subcompositor_get_subsurface( + wayl->sub_compositor, win->search_surface, win->surface); + wl_subsurface_set_sync(win->search_sub_surface); + term->search.original_view = term->grid->view; term->search.view_followed_offset = term->grid->view == term->grid->offset; term->is_searching = true; term_xcursor_update(term); - render_search_box(term); - render_refresh(term); + render_refresh_search(term); } void @@ -600,6 +615,5 @@ search_input(struct terminal *term, uint32_t key, xkb_keysym_t sym, xkb_mod_mask LOG_DBG("search: buffer: %S", term->search.buf); search_find_next(term); - render_refresh(term); - render_search_box(term); + render_refresh_search(term); } diff --git a/shm.c b/shm.c index 33095e74..56395fab 100644 --- a/shm.c +++ b/shm.c @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -124,13 +125,15 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie) size = stride * height; LOG_DBG("cookie=%lx: allocating new buffer: %zu KB", cookie, size / 1024); - int err = posix_fallocate(pool_fd, 0, size); + int err = EINTR; + while (err == EINTR) + err = posix_fallocate(pool_fd, 0, size); if (err != 0) { static bool failure_logged = false; if (!failure_logged) { failure_logged = true; - LOG_ERRNO_P("failed to fallocate", err); + LOG_ERRNO_P("failed to fallocate %zu bytes", err, size); } if (ftruncate(pool_fd, size) == -1) { diff --git a/sixel.c b/sixel.c index b538fddb..90d886fe 100644 --- a/sixel.c +++ b/sixel.c @@ -59,6 +59,11 @@ sixel_erase(struct terminal *term, struct sixel *sixel) int r = (sixel->pos.row + i) & (term->grid->num_rows - 1); struct row *row = term->grid->rows[r]; + if (row == NULL) { + /* A resize/reflow may cause row to now be unallocated */ + continue; + } + row->dirty = true; for (int c = 0; c < term->grid->num_cols; c++) diff --git a/slave.c b/slave.c index 2703c784..edd8e2cb 100644 --- a/slave.c +++ b/slave.c @@ -1,7 +1,9 @@ #include "slave.h" #include +#include #include +#include #include #include #include @@ -16,6 +18,52 @@ #include "tokenize.h" +static bool +is_valid_shell(const char *shell) +{ + FILE *f = fopen("/etc/shells", "r"); + if (f == NULL) + goto err; + + char *_line = NULL; + size_t count = 0; + + while (true) { + errno = 0; + ssize_t ret = getline(&_line, &count, f); + + if (ret < 0) { + free(_line); + break; + } + + char *line = _line; + { + while (isspace(*line)) + line++; + if (line[0] != '\0') { + char *end = line + strlen(line) - 1; + while (isspace(*end)) + end--; + *(end + 1) = '\0'; + } + } + + if (line[0] == '#') + continue; + + if (strcmp(line, shell) == 0) { + fclose(f); + return true; + } + } + +err: + if (f != NULL) + fclose(f); + return false; +} + static void slave_exec(int ptmx, char *argv[], int err_fd, bool login_shell) { @@ -145,6 +193,9 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, shell_argv[count] = NULL; } + if (is_valid_shell(shell_argv[0])) + setenv("SHELL", shell_argv[0], 1); + slave_exec(ptmx, shell_argv, fork_pipe[1], login_shell); assert(false); break; diff --git a/terminal.c b/terminal.c index 3977caae..7fc177f0 100644 --- a/terminal.c +++ b/terminal.c @@ -21,12 +21,14 @@ #include "async.h" #include "config.h" #include "grid.h" +#include "quirks.h" #include "render.h" #include "selection.h" #include "sixel.h" #include "slave.h" #include "vt.h" +#define ALEN(v) (sizeof(v) / sizeof(v[0])) #define min(x, y) ((x) < (y) ? (x) : (y)) #define max(x, y) ((x) > (y) ? (x) : (y)) @@ -197,7 +199,7 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) */ if (term->window->frame_callback == NULL) { if (term->render.app_sync_updates.enabled) - term->render.refresh_needed = true; + term->render.refresh.grid = true; else { /* First timeout - reset each time we receive input. */ @@ -233,7 +235,7 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) } } } else - term->render.pending = true; + term->render.pending.grid = true; if (hup) { if (term->hold_at_exit) { @@ -519,7 +521,8 @@ term_set_fonts(struct terminal *term, struct font *fonts[static 4]) term->cell_width = term->fonts[0]->space_x_advance > 0 ? term->fonts[0]->space_x_advance : term->fonts[0]->max_x_advance; - term->cell_height = term->fonts[0]->height; + term->cell_height = max(term->fonts[0]->height, + term->fonts[0]->ascent + term->fonts[0]->descent); LOG_INFO("cell width=%d, height=%d", term->cell_width, term->cell_height); render_resize_force(term, term->width, term->height); @@ -773,36 +776,13 @@ term_init(const struct config *conf, struct fdm *fdm, struct wayland *wayl, term_set_window_title(term, "foot"); /* Load fonts */ -#if 0 - struct font *fonts[4]; - if (!load_fonts_from_conf(term, conf, fonts)) + if (!term_font_dpi_changed(term)) goto err; - term_set_fonts(term, fonts); -#endif - term_font_dpi_changed(term); /* Start the slave/client */ if ((term->slave = slave_spawn(term->ptmx, argc, term->cwd, argv, term_env, conf->shell, login_shell)) == -1) goto err; - if (term->width == 0 && term->height == 0) { - - /* Try to use user-configured window dimentions */ - unsigned width = conf->width; - unsigned height = conf->height; - - if (width == -1) { - /* No user-configuration - use 80x24 cells */ - assert(height == -1); - width = 80 * term->cell_width; - height = 24 * term->cell_height; - } - - /* Don't go below a single cell */ - width = max(width, term->cell_width); - height = max(height, term->cell_height); - render_resize(term, width, height); - } return term; @@ -1657,6 +1637,7 @@ term_visual_focus_in(struct terminal *term) if (term->cursor_blink.active) cursor_blink_start_timer(term); + render_refresh_csd(term); cursor_refresh(term); } @@ -1670,6 +1651,7 @@ term_visual_focus_out(struct terminal *term) if (term->cursor_blink.active) cursor_blink_stop_timer(term); + render_refresh_csd(term); cursor_refresh(term); } @@ -2133,3 +2115,30 @@ term_print(struct terminal *term, wchar_t wc, int width) else term->cursor.lcf = true; } + +enum term_surface +term_surface_kind(const struct terminal *term, const struct wl_surface *surface) +{ + if (surface == term->window->surface) + return TERM_SURF_GRID; + else if (surface == term->window->search_surface) + return TERM_SURF_SEARCH; + else if (surface == term->window->csd.surface[CSD_SURF_TITLE]) + return TERM_SURF_TITLE; + else if (surface == term->window->csd.surface[CSD_SURF_LEFT]) + return TERM_SURF_BORDER_LEFT; + else if (surface == term->window->csd.surface[CSD_SURF_RIGHT]) + return TERM_SURF_BORDER_RIGHT; + else if (surface == term->window->csd.surface[CSD_SURF_TOP]) + return TERM_SURF_BORDER_TOP; + else if (surface == term->window->csd.surface[CSD_SURF_BOTTOM]) + return TERM_SURF_BORDER_BOTTOM; + else if (surface == term->window->csd.surface[CSD_SURF_MINIMIZE]) + return TERM_SURF_BUTTON_MINIMIZE; + else if (surface == term->window->csd.surface[CSD_SURF_MAXIMIZE]) + return TERM_SURF_BUTTON_MAXIMIZE; + else if (surface == term->window->csd.surface[CSD_SURF_CLOSE]) + return TERM_SURF_BUTTON_CLOSE; + else + return TERM_SURF_NONE; +} diff --git a/terminal.h b/terminal.h index 53fecf6b..92082d8c 100644 --- a/terminal.h +++ b/terminal.h @@ -185,6 +185,20 @@ struct sixel { struct coord pos; }; +enum term_surface { + TERM_SURF_NONE, + TERM_SURF_GRID, + TERM_SURF_SEARCH, + TERM_SURF_TITLE, + TERM_SURF_BORDER_LEFT, + TERM_SURF_BORDER_RIGHT, + TERM_SURF_BORDER_TOP, + TERM_SURF_BORDER_BOTTOM, + TERM_SURF_BUTTON_MINIMIZE, + TERM_SURF_BUTTON_MAXIMIZE, + TERM_SURF_BUTTON_CLOSE, +}; + struct terminal { struct fdm *fdm; const struct config *conf; @@ -237,6 +251,8 @@ struct terminal { int scale; int width; /* pixels */ int height; /* pixels */ + int unmaximized_width; /* last unmaximized size, pixels */ + int unmaximized_height; /* last unmaximized size, pixels */ struct { int left; int right; @@ -312,9 +328,23 @@ struct terminal { struct wayland *wl; struct wl_window *window; bool visual_focus; + enum term_surface active_surface; struct { - bool refresh_needed; /* Terminal needs to be re-rendered, as soon-as-possible */ + /* Scheduled for rendering, as soon-as-possible */ + struct { + bool grid; + bool csd; + bool search; + } refresh; + + /* Scheduled for rendering, in the next frame callback */ + struct { + bool grid; + bool csd; + bool search; + } pending; + int scrollback_lines; /* Number of scrollback lines, from conf (TODO: move out from render struct?) */ struct { @@ -341,7 +371,6 @@ struct terminal { struct cell *cell; /* For easy access to content */ } last_cursor; - bool pending; /* Need to re-render again, after next frame-callback */ struct buffer *last_buf; /* Buffer we rendered to last time */ bool was_flashing; /* Flash was active last time we rendered */ bool was_searching; @@ -477,3 +506,6 @@ bool term_spawn_new(const struct terminal *term); void term_enable_app_sync_updates(struct terminal *term); void term_disable_app_sync_updates(struct terminal *term); + +enum term_surface term_surface_kind( + const struct terminal *term, const struct wl_surface *surface); diff --git a/wayland.c b/wayland.c index 67757679..4bf77562 100644 --- a/wayland.c +++ b/wayland.c @@ -29,12 +29,51 @@ #include "render.h" #include "selection.h" +#define ALEN(v) (sizeof(v) / sizeof(v[0])) #define min(x, y) ((x) < (y) ? (x) : (y)) #define max(x, y) ((x) > (y) ? (x) : (y)) static bool wayl_reload_cursor_theme( struct wayland *wayl, struct terminal *term); +static void +csd_instantiate(struct wl_window *win) +{ + struct wayland *wayl = win->term->wl; + assert(wayl != NULL); + + for (size_t i = 0; i < ALEN(win->csd.surface); i++) { + assert(win->csd.surface[i] == NULL); + assert(win->csd.sub_surface[i] == NULL); + + win->csd.surface[i] = wl_compositor_create_surface(wayl->compositor); + + struct wl_surface *parent = i < CSD_SURF_MINIMIZE + ? win->surface : win->csd.surface[CSD_SURF_TITLE]; + + win->csd.sub_surface[i] = wl_subcompositor_get_subsurface( + wayl->sub_compositor, win->csd.surface[i], parent); + + wl_subsurface_set_sync(win->csd.sub_surface[i]); + wl_surface_set_user_data(win->csd.surface[i], win); + wl_surface_commit(win->csd.surface[i]); + } +} + +static void +csd_destroy(struct wl_window *win) +{ + for (size_t i = 0; i < ALEN(win->csd.surface); i++) { + if (win->csd.sub_surface[i] != NULL) + wl_subsurface_destroy(win->csd.sub_surface[i]); + if (win->csd.surface[i] != NULL) + wl_surface_destroy(win->csd.surface[i]); + + win->csd.surface[i] = NULL; + win->csd.sub_surface[i] = NULL; + } +} + static void shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) { @@ -64,24 +103,28 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, { struct wayland *wayl = data; - if (wayl->keyboard != NULL) { - wl_keyboard_release(wayl->keyboard); - wayl->keyboard = NULL; - } - - if (wayl->pointer.pointer != NULL) { - wl_pointer_release(wayl->pointer.pointer); - wayl->pointer.pointer = NULL; - } - if (caps & WL_SEAT_CAPABILITY_KEYBOARD) { - wayl->keyboard = wl_seat_get_keyboard(wl_seat); - wl_keyboard_add_listener(wayl->keyboard, &keyboard_listener, wayl); + if (wayl->keyboard == NULL) { + wayl->keyboard = wl_seat_get_keyboard(wl_seat); + wl_keyboard_add_listener(wayl->keyboard, &keyboard_listener, wayl); + } + } else { + if (wayl->keyboard != NULL) { + wl_keyboard_release(wayl->keyboard); + wayl->keyboard = NULL; + } } if (caps & WL_SEAT_CAPABILITY_POINTER) { - wayl->pointer.pointer = wl_seat_get_pointer(wl_seat); - wl_pointer_add_listener(wayl->pointer.pointer, &pointer_listener, wayl); + if (wayl->pointer.pointer == NULL) { + wayl->pointer.pointer = wl_seat_get_pointer(wl_seat); + wl_pointer_add_listener(wayl->pointer.pointer, &pointer_listener, wayl); + } + } else { + if (wayl->pointer.pointer != NULL) { + wl_pointer_release(wayl->pointer.pointer); + wayl->pointer.pointer = NULL; + } } } @@ -98,6 +141,9 @@ static const struct wl_seat_listener seat_listener = { static void update_term_for_output_change(struct terminal *term) { + if (tll_length(term->window->on_outputs) == 0) + return; + render_resize(term, term->width / term->scale, term->height / term->scale); term_font_dpi_changed(term); wayl_reload_cursor_theme(term->wl, term); @@ -322,7 +368,7 @@ handle_global(void *data, struct wl_registry *registry, } else if (strcmp(interface, wl_output_interface.name) == 0) { - const uint32_t required = 3; + const uint32_t required = 2; if (!verify_iface_version(interface, version, required)) return; @@ -425,6 +471,8 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height, struct wl_array *states) { bool is_activated = false; + bool is_fullscreen = false; + bool is_maximized = false; #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG char state_str[2048]; @@ -445,12 +493,10 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, enum xdg_toplevel_state *state; wl_array_for_each(state, states) { switch (*state) { - case XDG_TOPLEVEL_STATE_ACTIVATED: - is_activated = true; - break; + case XDG_TOPLEVEL_STATE_ACTIVATED: is_activated = true; break; + case XDG_TOPLEVEL_STATE_FULLSCREEN: is_fullscreen = true; break; + case XDG_TOPLEVEL_STATE_MAXIMIZED: is_maximized = true; break; - case XDG_TOPLEVEL_STATE_MAXIMIZED: - case XDG_TOPLEVEL_STATE_FULLSCREEN: case XDG_TOPLEVEL_STATE_RESIZING: case XDG_TOPLEVEL_STATE_TILED_LEFT: case XDG_TOPLEVEL_STATE_TILED_RIGHT: @@ -489,7 +535,18 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, * xdg_surface_configure() after we've ack:ed the event. */ struct wl_window *win = data; + + if (!is_fullscreen && win->use_csd == CSD_YES && width > 0 && height > 0) { + /* + * We include the CSD title bar in our window geometry. Thus, + * the height we call render_resize() with must be adjusted, + * since it expects the size to refer to the main grid only. + */ + height -= win->term->conf->csd.title_height; + } win->configure.is_activated = is_activated; + win->configure.is_fullscreen = is_fullscreen; + win->configure.is_maximized = is_maximized; win->configure.width = width; win->configure.height = height; } @@ -513,19 +570,37 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial) { LOG_DBG("xdg-surface: configure"); - xdg_surface_ack_configure(xdg_surface, serial); struct wl_window *win = data; struct terminal *term = win->term; win->is_configured = true; + win->is_maximized = win->configure.is_maximized; - render_resize(term, win->configure.width, win->configure.height); + if (win->is_fullscreen != win->configure.is_fullscreen && win->use_csd == CSD_YES) { + if (win->configure.is_fullscreen) + csd_destroy(win); + else + csd_instantiate(win); + } + win->is_fullscreen = win->configure.is_fullscreen; + + xdg_surface_ack_configure(xdg_surface, serial); + bool resized = render_resize(term, win->configure.width, win->configure.height); if (win->configure.is_activated) term_visual_focus_in(term); else term_visual_focus_out(term); + + if (!resized) { + /* + * If we didn't resize, we won't be commit a new surface + * anytime soon. Some compositors require a commit in + * combination with an ack - make them happy. + */ + wl_surface_commit(win->surface); + } } static const struct xdg_surface_listener xdg_surface_listener = { @@ -537,19 +612,37 @@ xdg_toplevel_decoration_configure(void *data, struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, uint32_t mode) { + struct wl_window *win = data; + switch (mode) { case ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE: - LOG_ERR("unimplemented: client-side decorations"); + LOG_INFO("using CSD decorations"); + win->use_csd = CSD_YES; + csd_instantiate(win); break; case ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE: - LOG_DBG("using server-side decorations"); + LOG_INFO("using SSD decorations"); + win->use_csd = CSD_NO; + csd_destroy(win); break; default: LOG_ERR("unimplemented: unknown XDG toplevel decoration mode: %u", mode); break; } + + if (win->is_configured && win->use_csd == CSD_YES) { + struct terminal *term = win->term; + + int scale = term->scale; + int width = term->width / scale; + int height = term->height / scale; + + /* Take CSD title bar into account */ + height -= term->conf->csd.title_height; + render_resize_force(term, width, height); + } } static const struct zxdg_toplevel_decoration_v1_listener xdg_toplevel_decoration_listener = { @@ -879,9 +972,12 @@ struct wl_window * wayl_win_init(struct terminal *term) { struct wayland *wayl = term->wl; + const struct config *conf = wayl->conf; struct wl_window *win = calloc(1, sizeof(*win)); win->term = term; + win->use_csd = CSD_UNKNOWN; + win->csd.move_timeout_fd = -1; win->surface = wl_compositor_create_surface(wayl->compositor); if (win->surface == NULL) { @@ -911,18 +1007,27 @@ wayl_win_init(struct terminal *term) xdg_toplevel_set_app_id(win->xdg_toplevel, "foot"); /* Request server-side decorations */ - win->xdg_toplevel_decoration = zxdg_decoration_manager_v1_get_toplevel_decoration( - wayl->xdg_decoration_manager, win->xdg_toplevel); - zxdg_toplevel_decoration_v1_set_mode( - win->xdg_toplevel_decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); - zxdg_toplevel_decoration_v1_add_listener( - win->xdg_toplevel_decoration, &xdg_toplevel_decoration_listener, win); + if (wayl->xdg_decoration_manager != NULL) { + win->xdg_toplevel_decoration = zxdg_decoration_manager_v1_get_toplevel_decoration( + wayl->xdg_decoration_manager, win->xdg_toplevel); - /* Scrollback search box */ - win->search_surface = wl_compositor_create_surface(wayl->compositor); - win->search_sub_surface = wl_subcompositor_get_subsurface( - wayl->sub_compositor, win->search_surface, win->surface); - wl_subsurface_set_desync(win->search_sub_surface); + LOG_INFO("requesting %s decorations", + conf->csd.preferred == CONF_CSD_PREFER_SERVER ? "SSD" : "CSD"); + + zxdg_toplevel_decoration_v1_set_mode( + win->xdg_toplevel_decoration, + (conf->csd.preferred == CONF_CSD_PREFER_SERVER + ? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE + : ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE)); + + zxdg_toplevel_decoration_v1_add_listener( + win->xdg_toplevel_decoration, &xdg_toplevel_decoration_listener, win); + } else { + /* No decoration manager - thus we *must* draw our own decorations */ + win->use_csd = CSD_YES; + csd_instantiate(win); + LOG_WARN("no decoration manager available - using CSDs unconditionally"); + } wl_surface_commit(win->surface); return win; @@ -939,6 +1044,9 @@ wayl_win_destroy(struct wl_window *win) if (win == NULL) return; + if (win->csd.move_timeout_fd != -1) + close(win->csd.move_timeout_fd); + /* * First, unmap all surfaces to trigger things like * keyboard_leave() and wl_pointer_leave(). @@ -949,16 +1057,29 @@ wayl_win_destroy(struct wl_window *win) */ /* Scrollback search */ - wl_surface_attach(win->search_surface, NULL, 0, 0); - wl_surface_commit(win->search_surface); + if (win->search_surface != NULL) { + wl_surface_attach(win->search_surface, NULL, 0, 0); + wl_surface_commit(win->search_surface); + } + + /* CSD */ + for (size_t i = 0; i < ALEN(win->csd.surface); i++) { + if (win->csd.surface[i] != NULL) { + wl_surface_attach(win->csd.surface[i], NULL, 0, 0); + wl_surface_commit(win->csd.surface[i]); + } + } + wayl_roundtrip(win->term->wl); - /* Main window */ + /* Main window */ wl_surface_attach(win->surface, NULL, 0, 0); wl_surface_commit(win->surface); wayl_roundtrip(win->term->wl); tll_free(win->on_outputs); + + csd_destroy(win); if (win->search_sub_surface != NULL) wl_subsurface_destroy(win->search_sub_surface); if (win->search_surface != NULL) @@ -1004,22 +1125,6 @@ wayl_reload_cursor_theme(struct wayland *wayl, struct terminal *term) return render_xcursor_set(term); } -struct terminal * -wayl_terminal_from_surface(struct wayland *wayl, struct wl_surface *surface) -{ - tll_foreach(wayl->terms, it) { - if (it->item->window->surface == surface || - it->item->window->search_surface == surface) - { - return it->item; - } - } - - assert(false); - LOG_WARN("surface %p doesn't map to a terminal", surface); - return NULL; -} - void wayl_flush(struct wayland *wayl) { diff --git a/wayland.h b/wayland.h index 83a84273..ed53591d 100644 --- a/wayland.h +++ b/wayland.h @@ -82,6 +82,18 @@ struct wl_primary { uint32_t serial; }; +enum csd_surface { + CSD_SURF_TITLE, + CSD_SURF_LEFT, + CSD_SURF_RIGHT, + CSD_SURF_TOP, + CSD_SURF_BOTTOM, + CSD_SURF_MINIMIZE, + CSD_SURF_MAXIMIZE, + CSD_SURF_CLOSE, + CSD_SURF_COUNT, +}; + struct wayland; struct wl_window { struct terminal *term; @@ -91,6 +103,15 @@ struct wl_window { struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; + enum {CSD_UNKNOWN, CSD_NO, CSD_YES } use_csd; + + struct { + struct wl_surface *surface[CSD_SURF_COUNT]; + struct wl_subsurface *sub_surface[CSD_SURF_COUNT]; + int move_timeout_fd; + uint32_t serial; + } csd; + /* Scrollback search */ struct wl_surface *search_surface; struct wl_subsurface *search_sub_surface; @@ -100,8 +121,12 @@ struct wl_window { tll(const struct monitor *) on_outputs; /* Outputs we're mapped on */ bool is_configured; + bool is_fullscreen; + bool is_maximized; struct { bool is_activated; + bool is_fullscreen; + bool is_maximized; int width; int height; } configure; @@ -158,6 +183,8 @@ struct wayland { } pointer; struct { + int x; + int y; int col; int row; int button; @@ -184,8 +211,5 @@ void wayl_destroy(struct wayland *wayl); void wayl_flush(struct wayland *wayl); void wayl_roundtrip(struct wayland *wayl); -struct terminal *wayl_terminal_from_surface( - struct wayland *wayl, struct wl_surface *surface); - struct wl_window *wayl_win_init(struct terminal *term); void wayl_win_destroy(struct wl_window *win);