diff --git a/docs/labwc-actions.5.scd b/docs/labwc-actions.5.scd index d9defff1..1a65bcda 100644 --- a/docs/labwc-actions.5.scd +++ b/docs/labwc-actions.5.scd @@ -112,11 +112,13 @@ Actions are used in menus and keyboard/mouse bindings. ** Toggle fullscreen state of focused window. -** - Toggle maximize state of focused window. +** + Toggle maximize state of focused window. Supported directions are + "both" (default), "horizontal", and "vertical". -** - Maximize focused window. +** + Maximize focused window. Supported directions are "both" (default), + "horizontal", and "vertical". ** Toggle always-on-top of focused window. diff --git a/docs/rc.xml.all b/docs/rc.xml.all index 7a44ea39..973a9701 100644 --- a/docs/rc.xml.all +++ b/docs/rc.xml.all @@ -298,7 +298,6 @@ - @@ -309,20 +308,30 @@ + + + - - + + + + + + + + + diff --git a/include/view.h b/include/view.h index 471e52dd..b2d8ce31 100644 --- a/include/view.h +++ b/include/view.h @@ -29,6 +29,18 @@ enum ssd_preference { LAB_SSD_PREF_SERVER, }; +/** + * Directions in which a view can be maximized. "None" is used + * internally to mean "not maximized" but is not valid in rc.xml. + * Therefore when parsing rc.xml, "None" means "Invalid". + */ +enum view_axis { + VIEW_AXIS_NONE = 0, + VIEW_AXIS_HORIZONTAL = (1 << 0), + VIEW_AXIS_VERTICAL = (1 << 1), + VIEW_AXIS_BOTH = (VIEW_AXIS_HORIZONTAL | VIEW_AXIS_VERTICAL), +}; + enum view_edge { VIEW_EDGE_INVALID = 0, @@ -127,7 +139,7 @@ struct view { bool ssd_titlebar_hidden; enum ssd_preference ssd_preference; bool minimized; - bool maximized; + enum view_axis maximized; bool fullscreen; uint32_t tiled; /* private, enum view_edge in src/view.c */ bool inhibits_keybinds; @@ -353,10 +365,10 @@ void view_store_natural_geometry(struct view *view); void view_center(struct view *view, const struct wlr_box *ref); void view_restore_to(struct view *view, struct wlr_box geometry); void view_set_untiled(struct view *view); -void view_maximize(struct view *view, bool maximize, +void view_maximize(struct view *view, enum view_axis axis, bool store_natural_geometry); void view_set_fullscreen(struct view *view, bool fullscreen); -void view_toggle_maximize(struct view *view); +void view_toggle_maximize(struct view *view, enum view_axis axis); void view_toggle_decorations(struct view *view); bool view_is_always_on_top(struct view *view); @@ -400,6 +412,7 @@ void view_evacuate_region(struct view *view); void view_on_output_destroy(struct view *view); void view_destroy(struct view *view); +enum view_axis view_axis_parse(const char *direction); enum view_edge view_edge_parse(const char *direction); /* xdg.c */ diff --git a/src/action.c b/src/action.c index 8b67403f..26d609bb 100644 --- a/src/action.c +++ b/src/action.c @@ -296,6 +296,19 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char goto cleanup; } break; + case ACTION_TYPE_TOGGLE_MAXIMIZE: + case ACTION_TYPE_MAXIMIZE: + if (!strcmp(argument, "direction")) { + enum view_axis axis = view_axis_parse(content); + if (axis == VIEW_AXIS_NONE) { + wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", + action_names[action->type], argument, content); + } else { + action_arg_add_int(action, argument, axis); + } + goto cleanup; + } + break; case ACTION_TYPE_RESIZE_RELATIVE: if (!strcmp(argument, "left") || !strcmp(argument, "right") || !strcmp(argument, "top") || !strcmp(argument, "bottom")) { @@ -694,12 +707,17 @@ actions_run(struct view *activator, struct server *server, break; case ACTION_TYPE_TOGGLE_MAXIMIZE: if (view) { - view_toggle_maximize(view); + enum view_axis axis = action_get_int(action, + "direction", VIEW_AXIS_BOTH); + view_toggle_maximize(view, axis); } break; case ACTION_TYPE_MAXIMIZE: if (view) { - view_maximize(view, true, /*store_natural_geometry*/ true); + enum view_axis axis = action_get_int(action, + "direction", VIEW_AXIS_BOTH); + view_maximize(view, axis, + /*store_natural_geometry*/ true); } break; case ACTION_TYPE_TOGGLE_FULLSCREEN: diff --git a/src/config/rcxml.c b/src/config/rcxml.c index 0f8547ba..b834ea5d 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -1064,11 +1064,14 @@ static struct mouse_combos { { "Title", "Left", "DoubleClick", "ToggleMaximize", NULL, NULL }, { "TitleBar", "Right", "Click", "Focus", NULL, NULL}, { "TitleBar", "Right", "Click", "Raise", NULL, NULL}, - { "TitleBar", "Right", "Click", "ShowMenu", "menu", "client-menu"}, + { "Title", "Right", "Click", "ShowMenu", "menu", "client-menu"}, { "Close", "Left", "Click", "Close", NULL, NULL }, { "Iconify", "Left", "Click", "Iconify", NULL, NULL}, { "Maximize", "Left", "Click", "ToggleMaximize", NULL, NULL}, + { "Maximize", "Right", "Click", "ToggleMaximize", "direction", "horizontal"}, + { "Maximize", "Middle", "Click", "ToggleMaximize", "direction", "vertical"}, { "WindowMenu", "Left", "Click", "ShowMenu", "menu", "client-menu"}, + { "WindowMenu", "Right", "Click", "ShowMenu", "menu", "client-menu"}, { "Root", "Left", "Press", "ShowMenu", "menu", "root-menu"}, { "Root", "Right", "Press", "ShowMenu", "menu", "root-menu"}, { "Root", "Middle", "Press", "ShowMenu", "menu", "root-menu"}, diff --git a/src/foreign.c b/src/foreign.c index e71292d7..092752b3 100644 --- a/src/foreign.c +++ b/src/foreign.c @@ -17,7 +17,8 @@ handle_request_maximize(struct wl_listener *listener, void *data) { struct view *view = wl_container_of(listener, view, toplevel.maximize); struct wlr_foreign_toplevel_handle_v1_maximized_event *event = data; - view_maximize(view, event->maximized, /*store_natural_geometry*/ true); + view_maximize(view, event->maximized ? VIEW_AXIS_BOTH : VIEW_AXIS_NONE, + /*store_natural_geometry*/ true); } static void diff --git a/src/interactive.c b/src/interactive.c index 3caac79e..0476f5fb 100644 --- a/src/interactive.c +++ b/src/interactive.c @@ -45,7 +45,7 @@ interactive_begin(struct view *view, enum input_mode mode, uint32_t edges) */ return; } - if (view->maximized || view_is_tiled(view)) { + if (!view_is_floating(view)) { /* * Un-maximize and restore natural width/height. * Don't reset tiled state yet since we may want @@ -71,18 +71,20 @@ interactive_begin(struct view *view, enum input_mode mode, uint32_t edges) cursor_set(seat, LAB_CURSOR_GRAB); break; case LAB_INPUT_STATE_RESIZE: - if (view->maximized || view->fullscreen) { + if (view->fullscreen || view->maximized == VIEW_AXIS_BOTH) { /* - * We don't allow resizing while maximized or - * fullscreen. + * We don't allow resizing while fullscreen or + * maximized in both directions. */ return; } /* - * Reset tiled state but keep the same geometry as the - * starting point for the resize. + * If tiled or maximized in only one direction, reset + * tiled/maximized state but keep the same geometry as + * the starting point for the resize. */ view_set_untiled(view); + view_restore_to(view, view->pending); cursor_set(seat, cursor_get_from_edge(edges)); break; default: @@ -130,7 +132,7 @@ snap_to_edge(struct view *view) /*store_natural_geometry*/ false); } else if (cursor_y <= area->y + snap_range) { if (rc.snap_top_maximize) { - view_maximize(view, true, + view_maximize(view, VIEW_AXIS_BOTH, /*store_natural_geometry*/ false); } else { view_snap_to_edge(view, VIEW_EDGE_UP, diff --git a/src/snap.c b/src/snap.c index b403898d..698cbf3b 100644 --- a/src/snap.c +++ b/src/snap.c @@ -102,7 +102,8 @@ _snap_next_edge(struct view *view, int start_pos, const struct snap_search def, struct view *v; int p = max; for_each_view(v, &server->views, LAB_VIEW_CRITERIA_CURRENT_WORKSPACE) { - if (v == view || v->output != output || v->minimized || v->maximized) { + if (v == view || v->output != output || v->minimized + || v->maximized == VIEW_AXIS_BOTH) { continue; } diff --git a/src/ssd/ssd.c b/src/ssd/ssd.c index ef821b86..4580ec2f 100644 --- a/src/ssd/ssd.c +++ b/src/ssd/ssd.c @@ -35,7 +35,7 @@ ssd_thickness(struct view *view) struct theme *theme = view->server->theme; - if (view->maximized) { + if (view->maximized == VIEW_AXIS_BOTH) { struct border thickness = { 0 }; if (!view->ssd_titlebar_hidden) { thickness.top += theme->title_height; @@ -226,7 +226,8 @@ ssd_update_geometry(struct ssd *ssd) ssd_extents_update(ssd); ssd->state.geometry = current; } - if (ssd->state.squared_corners != ssd->view->maximized) { + bool maximized = (ssd->view->maximized == VIEW_AXIS_BOTH); + if (ssd->state.squared_corners != maximized) { ssd_border_update(ssd); ssd_titlebar_update(ssd); } diff --git a/src/ssd/ssd_border.c b/src/ssd/ssd_border.c index 6512ab82..40be4f39 100644 --- a/src/ssd/ssd_border.c +++ b/src/ssd/ssd_border.c @@ -53,7 +53,7 @@ ssd_border_create(struct ssd *ssd) -(ssd->titlebar.height + theme->border_width), color); } FOR_EACH_END - if (view->maximized) { + if (view->maximized == VIEW_AXIS_BOTH) { wlr_scene_node_set_enabled(&ssd->border.tree->node, false); } } @@ -65,13 +65,14 @@ ssd_border_update(struct ssd *ssd) assert(ssd->border.tree); struct view *view = ssd->view; - if (view->maximized && ssd->border.tree->node.enabled) { + if (view->maximized == VIEW_AXIS_BOTH + && ssd->border.tree->node.enabled) { /* Disable borders on maximize */ wlr_scene_node_set_enabled(&ssd->border.tree->node, false); ssd->margin = ssd_thickness(ssd->view); } - if (view->maximized) { + if (view->maximized == VIEW_AXIS_BOTH) { return; } else if (!ssd->border.tree->node.enabled) { /* And re-enabled them when unmaximized */ diff --git a/src/ssd/ssd_extents.c b/src/ssd/ssd_extents.c index 1b6f465d..05869e21 100644 --- a/src/ssd/ssd_extents.c +++ b/src/ssd/ssd_extents.c @@ -35,7 +35,7 @@ ssd_extents_create(struct ssd *ssd) ssd->extents.tree = wlr_scene_tree_create(ssd->tree); struct wlr_scene_tree *parent = ssd->extents.tree; - if (view->maximized || view->fullscreen) { + if (view->fullscreen || view->maximized == VIEW_AXIS_BOTH) { wlr_scene_node_set_enabled(&parent->node, false); } wl_list_init(&ssd->extents.parts); @@ -89,7 +89,7 @@ void ssd_extents_update(struct ssd *ssd) { struct view *view = ssd->view; - if (view->maximized || view->fullscreen) { + if (view->fullscreen || view->maximized == VIEW_AXIS_BOTH) { wlr_scene_node_set_enabled(&ssd->extents.tree->node, false); return; } diff --git a/src/ssd/ssd_titlebar.c b/src/ssd/ssd_titlebar.c index b0aaa2d0..62ac232c 100644 --- a/src/ssd/ssd_titlebar.c +++ b/src/ssd/ssd_titlebar.c @@ -86,8 +86,8 @@ ssd_titlebar_create(struct ssd *ssd) ssd_update_title(ssd); - if (view->maximized) { - set_squared_corners(ssd, view->maximized); + if (view->maximized == VIEW_AXIS_BOTH) { + set_squared_corners(ssd, true); } } @@ -133,8 +133,9 @@ ssd_titlebar_update(struct ssd *ssd) int width = view->current.width; struct theme *theme = view->server->theme; - if (view->maximized != ssd->state.squared_corners) { - set_squared_corners(ssd, view->maximized); + bool maximized = (view->maximized == VIEW_AXIS_BOTH); + if (ssd->state.squared_corners != maximized) { + set_squared_corners(ssd, maximized); } if (width == ssd->state.geometry.width) { diff --git a/src/view.c b/src/view.c index 106a2aa7..060816da 100644 --- a/src/view.c +++ b/src/view.c @@ -337,7 +337,7 @@ void view_resize_relative(struct view *view, int left, int right, int top, int bottom) { assert(view); - if (view->fullscreen || view->maximized) { + if (view->fullscreen || view->maximized != VIEW_AXIS_NONE) { return; } struct wlr_box newgeo = view->pending; @@ -356,7 +356,7 @@ view_move_relative(struct view *view, int x, int y) if (view->fullscreen) { return; } - view_maximize(view, false, /*store_natural_geometry*/ false); + view_maximize(view, VIEW_AXIS_NONE, /*store_natural_geometry*/ false); if (view_is_tiled(view)) { view_set_untiled(view); view_restore_to(view, view->natural_geometry); @@ -373,12 +373,8 @@ view_move_to_cursor(struct view *view) if (!output_is_usable(pending_output)) { return; } - if (view->fullscreen) { - view_set_fullscreen(view, false); - } - if (view->maximized) { - view_maximize(view, false, /*store_natural_geometry*/ false); - } + view_set_fullscreen(view, false); + view_maximize(view, VIEW_AXIS_NONE, /*store_natural_geometry*/ false); if (view_is_tiled(view)) { view_set_untiled(view); view_restore_to(view, view->natural_geometry); @@ -739,7 +735,7 @@ static void view_apply_maximized_geometry(struct view *view) { assert(view); - assert(view->maximized); + assert(view->maximized != VIEW_AXIS_NONE); struct output *output = view->output; assert(output_is_usable(output)); @@ -753,6 +749,23 @@ view_apply_maximized_geometry(struct view *view) box.width /= output->wlr_output->scale; } + /* + * If one axis (horizontal or vertical) is unmaximized, it + * should use the natural geometry. But if that geometry is not + * on-screen on the output where the view is maximized, then + * center the unmaximized axis. + */ + struct wlr_box natural = view->natural_geometry; + if (view->maximized != VIEW_AXIS_BOTH) { + struct wlr_box intersect; + wlr_box_intersection(&intersect, &box, &natural); + if (wlr_box_empty(&intersect)) { + view_compute_centered_position(view, NULL, + natural.width, natural.height, + &natural.x, &natural.y); + } + } + if (view->ssd_enabled) { struct border border = ssd_thickness(view); box.x += border.left; @@ -760,6 +773,15 @@ view_apply_maximized_geometry(struct view *view) box.width -= border.right + border.left; box.height -= border.top + border.bottom; } + + if (view->maximized == VIEW_AXIS_VERTICAL) { + box.x = natural.x; + box.width = natural.width; + } else if (view->maximized == VIEW_AXIS_HORIZONTAL) { + box.y = natural.y; + box.height = natural.height; + } + view_move_resize(view, box); } @@ -775,7 +797,7 @@ view_apply_special_geometry(struct view *view) if (view->fullscreen) { view_apply_fullscreen_geometry(view); - } else if (view->maximized) { + } else if (view->maximized != VIEW_AXIS_NONE) { view_apply_maximized_geometry(view); } else if (view->tiled) { view_apply_tiled_geometry(view); @@ -788,14 +810,14 @@ view_apply_special_geometry(struct view *view) /* For internal use only. Does not update geometry. */ static void -set_maximized(struct view *view, bool maximized) +set_maximized(struct view *view, enum view_axis maximized) { if (view->impl->maximize) { - view->impl->maximize(view, maximized); + view->impl->maximize(view, (maximized == VIEW_AXIS_BOTH)); } if (view->toplevel.handle) { wlr_foreign_toplevel_handle_v1_set_maximized( - view->toplevel.handle, maximized); + view->toplevel.handle, (maximized == VIEW_AXIS_BOTH)); } view->maximized = maximized; @@ -818,8 +840,8 @@ view_restore_to(struct view *view, struct wlr_box geometry) if (view->fullscreen) { return; } - if (view->maximized) { - set_maximized(view, false); + if (view->maximized != VIEW_AXIS_NONE) { + set_maximized(view, VIEW_AXIS_NONE); } view_move_resize(view, geometry); } @@ -836,8 +858,8 @@ bool view_is_floating(struct view *view) { assert(view); - return !(view->fullscreen || view->maximized || view->tiled - || view->tiled_region || view->tiled_region_evacuate); + return !(view->fullscreen || (view->maximized != VIEW_AXIS_NONE) + || view_is_tiled(view)); } /* Reset tiled state of view without changing geometry */ @@ -851,27 +873,28 @@ view_set_untiled(struct view *view) } void -view_maximize(struct view *view, bool maximize, bool store_natural_geometry) +view_maximize(struct view *view, enum view_axis axis, + bool store_natural_geometry) { assert(view); - if (view->maximized == maximize) { + if (view->maximized == axis) { return; } if (view->fullscreen) { return; } - if (maximize) { + if (axis != VIEW_AXIS_NONE) { /* * Maximize via keybind or client request cancels * interactive move/resize since we can't move/resize * a maximized view. */ interactive_cancel(view); - if (store_natural_geometry) { + if (store_natural_geometry && view_is_floating(view)) { view_store_natural_geometry(view); } } - set_maximized(view, maximize); + set_maximized(view, axis); if (view_is_floating(view)) { view_apply_natural_geometry(view); } else { @@ -880,11 +903,28 @@ view_maximize(struct view *view, bool maximize, bool store_natural_geometry) } void -view_toggle_maximize(struct view *view) +view_toggle_maximize(struct view *view, enum view_axis axis) { assert(view); - view_maximize(view, !view->maximized, - /*store_natural_geometry*/ true); + switch (axis) { + case VIEW_AXIS_HORIZONTAL: + case VIEW_AXIS_VERTICAL: + /* Toggle one axis (XOR) */ + view_maximize(view, view->maximized ^ axis, + /*store_natural_geometry*/ true); + break; + case VIEW_AXIS_BOTH: + /* + * Maximize in both directions if unmaximized or partially + * maximized, otherwise unmaximize. + */ + view_maximize(view, (view->maximized == VIEW_AXIS_BOTH) ? + VIEW_AXIS_NONE : VIEW_AXIS_BOTH, + /*store_natural_geometry*/ true); + break; + default: + break; + } } void @@ -1167,7 +1207,8 @@ void view_grow_to_edge(struct view *view, enum view_edge direction) { assert(view); - if (view->fullscreen || view->maximized) { + /* TODO: allow grow to edge if maximized along the other axis */ + if (view->fullscreen || view->maximized != VIEW_AXIS_NONE) { return; } if (!output_is_usable(view->output)) { @@ -1184,7 +1225,8 @@ void view_shrink_to_edge(struct view *view, enum view_edge direction) { assert(view); - if (view->fullscreen || view->maximized) { + /* TODO: allow shrink to edge if maximized along the other axis */ + if (view->fullscreen || view->maximized != VIEW_AXIS_NONE) { return; } if (!output_is_usable(view->output)) { @@ -1197,6 +1239,23 @@ view_shrink_to_edge(struct view *view, enum view_edge direction) view_move_resize(view, geo); } +enum view_axis +view_axis_parse(const char *direction) +{ + if (!direction) { + return VIEW_AXIS_NONE; + } + if (!strcasecmp(direction, "horizontal")) { + return VIEW_AXIS_HORIZONTAL; + } else if (!strcasecmp(direction, "vertical")) { + return VIEW_AXIS_VERTICAL; + } else if (!strcasecmp(direction, "both")) { + return VIEW_AXIS_BOTH; + } else { + return VIEW_AXIS_NONE; + } +} + enum view_edge view_edge_parse(const char *direction) { @@ -1231,7 +1290,7 @@ view_snap_to_edge(struct view *view, enum view_edge edge, bool store_natural_geo return; } - if (view->tiled == edge && !view->maximized) { + if (view->tiled == edge && view->maximized == VIEW_AXIS_NONE) { /* We are already tiled for this edge and thus should switch outputs */ struct wlr_output *new_output = NULL; struct wlr_output *current_output = output->wlr_output; @@ -1285,9 +1344,10 @@ view_snap_to_edge(struct view *view, enum view_edge edge, bool store_natural_geo } } - if (view->maximized) { + if (view->maximized != VIEW_AXIS_NONE) { /* Unmaximize + keep using existing natural_geometry */ - view_maximize(view, false, /*store_natural_geometry*/ false); + view_maximize(view, VIEW_AXIS_NONE, + /*store_natural_geometry*/ false); } else if (store_natural_geometry) { /* store current geometry as new natural_geometry */ view_store_natural_geometry(view); @@ -1313,9 +1373,10 @@ view_snap_to_region(struct view *view, struct region *region, return; } - if (view->maximized) { + if (view->maximized != VIEW_AXIS_NONE) { /* Unmaximize + keep using existing natural_geometry */ - view_maximize(view, false, /*store_natural_geometry*/ false); + view_maximize(view, VIEW_AXIS_NONE, + /*store_natural_geometry*/ false); } else if (store_natural_geometry) { /* store current geometry as new natural_geometry */ view_store_natural_geometry(view); diff --git a/src/xdg.c b/src/xdg.c index f63647e1..dc9be334 100644 --- a/src/xdg.c +++ b/src/xdg.c @@ -226,7 +226,8 @@ handle_request_maximize(struct wl_listener *listener, void *data) if (!view->mapped && !view->output) { view_set_output(view, output_nearest_to_cursor(view->server)); } - view_maximize(view, xdg_toplevel_from_view(view)->requested.maximized, + bool maximized = xdg_toplevel_from_view(view)->requested.maximized; + view_maximize(view, maximized ? VIEW_AXIS_BOTH : VIEW_AXIS_NONE, /*store_natural_geometry*/ true); } @@ -503,12 +504,10 @@ xdg_toplevel_view_map(struct view *view) position_xdg_toplevel_view(view); } - if (!view->fullscreen && requested->fullscreen) { - set_fullscreen_from_request(view, requested); - } else if (!view->maximized && requested->maximized) { - view_maximize(view, true, - /*store_natural_geometry*/ true); - } + set_fullscreen_from_request(view, requested); + view_maximize(view, requested->maximized ? + VIEW_AXIS_BOTH : VIEW_AXIS_NONE, + /*store_natural_geometry*/ true); /* * Set initial "current" position directly before diff --git a/src/xwayland.c b/src/xwayland.c index 020c1ece..6e4fae7f 100644 --- a/src/xwayland.c +++ b/src/xwayland.c @@ -351,7 +351,7 @@ handle_request_maximize(struct wl_listener *listener, void *data) view_set_decorations(view, want_deco(xwayland_surface_from_view(view))); } - view_toggle_maximize(view); + view_toggle_maximize(view, VIEW_AXIS_BOTH); } static void @@ -540,14 +540,17 @@ xwayland_view_map(struct view *view) * 1. set fullscreen state * 2. set decorations (depends on fullscreen state) * 3. set maximized (geometry depends on decorations) - * - * TODO: support separate horizontal/vertical maximize */ - bool maximize = xwayland_surface->maximized_horz - && xwayland_surface->maximized_vert; view_set_fullscreen(view, xwayland_surface->fullscreen); view_set_decorations(view, want_deco(xwayland_surface)); - view_maximize(view, maximize, /*store_natural_geometry*/ true); + enum view_axis axis = VIEW_AXIS_NONE; + if (xwayland_surface->maximized_horz) { + axis |= VIEW_AXIS_HORIZONTAL; + } + if (xwayland_surface->maximized_vert) { + axis |= VIEW_AXIS_VERTICAL; + } + view_maximize(view, axis, /*store_natural_geometry*/ true); if (view->surface != xwayland_surface->surface) { if (view->surface) {