mirror of
https://github.com/labwc/labwc.git
synced 2025-10-29 05:40:24 -04:00
Some checks failed
labwc.github.io / notify (push) Has been cancelled
When implementing single-axis maximize some time ago, I made the simplifying assumption that a view couldn't be resized while maximized (even in only one axis). And indeed for compositor-initiated resize, we always unmaximize the view first. However, I didn't account for the client resizing the non-maximized axis, which we can't (and shouldn't) prevent. When this happens, we should also update the natural geometry of that single axis so that we don't undo the resize when un-maximizing. P.S. xdg-shell clients resizing the *maximized* axis is still an unsolved problem, exacerbated by the fact that xdg-shell protocol doesn't allow clients to even know about single-axis maximize. P.P.S. the view_invalidate_last_layout_geometry() logic may need similar updates, I'm not sure.
2660 lines
64 KiB
C
2660 lines
64 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
#include "view.h"
|
|
#include <assert.h>
|
|
#include <strings.h>
|
|
#include <wlr/types/wlr_keyboard_group.h>
|
|
#include <wlr/types/wlr_output_layout.h>
|
|
#include <wlr/types/wlr_scene.h>
|
|
#include <wlr/types/wlr_security_context_v1.h>
|
|
#include <wlr/types/wlr_xdg_shell.h>
|
|
#include "action.h"
|
|
#include "buffer.h"
|
|
#include "common/box.h"
|
|
#include "common/list.h"
|
|
#include "common/match.h"
|
|
#include "common/mem.h"
|
|
#include "common/scene-helpers.h"
|
|
#include "config/rcxml.h"
|
|
#include "foreign-toplevel/foreign.h"
|
|
#include "input/keyboard.h"
|
|
#include "labwc.h"
|
|
#include "menu/menu.h"
|
|
#include "osd.h"
|
|
#include "output.h"
|
|
#include "output-state.h"
|
|
#include "placement.h"
|
|
#include "regions.h"
|
|
#include "resize-indicator.h"
|
|
#include "session-lock.h"
|
|
#include "snap-constraints.h"
|
|
#include "snap.h"
|
|
#include "ssd.h"
|
|
#include "theme.h"
|
|
#include "window-rules.h"
|
|
#include "wlr/util/log.h"
|
|
#include "workspaces.h"
|
|
#include "xwayland.h"
|
|
|
|
#if HAVE_XWAYLAND
|
|
#include <wlr/xwayland.h>
|
|
#endif
|
|
|
|
struct view *
|
|
view_from_wlr_surface(struct wlr_surface *surface)
|
|
{
|
|
assert(surface);
|
|
/*
|
|
* TODO:
|
|
* - find a way to get rid of xdg/xwayland-specific stuff
|
|
* - look up root/toplevel surface if passed a subsurface?
|
|
*/
|
|
struct wlr_xdg_surface *xdg_surface =
|
|
wlr_xdg_surface_try_from_wlr_surface(surface);
|
|
if (xdg_surface) {
|
|
return xdg_surface->data;
|
|
}
|
|
#if HAVE_XWAYLAND
|
|
struct wlr_xwayland_surface *xsurface =
|
|
wlr_xwayland_surface_try_from_wlr_surface(surface);
|
|
if (xsurface) {
|
|
return xsurface->data;
|
|
}
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
static const struct wlr_security_context_v1_state *
|
|
security_context_from_view(struct view *view)
|
|
{
|
|
if (view && view->surface && view->surface->resource) {
|
|
struct wl_client *client = wl_resource_get_client(view->surface->resource);
|
|
return wlr_security_context_manager_v1_lookup_client(
|
|
view->server->security_context_manager_v1, client);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
struct view_query *
|
|
view_query_create(void)
|
|
{
|
|
struct view_query *query = znew(*query);
|
|
query->window_type = -1;
|
|
query->maximized = VIEW_AXIS_INVALID;
|
|
query->decoration = LAB_SSD_MODE_INVALID;
|
|
return query;
|
|
}
|
|
|
|
void
|
|
view_query_free(struct view_query *query)
|
|
{
|
|
wl_list_remove(&query->link);
|
|
zfree(query->identifier);
|
|
zfree(query->title);
|
|
zfree(query->sandbox_engine);
|
|
zfree(query->sandbox_app_id);
|
|
zfree(query->tiled_region);
|
|
zfree(query->desktop);
|
|
zfree(query->monitor);
|
|
zfree(query);
|
|
}
|
|
|
|
static bool
|
|
query_tristate_match(enum lab_tristate desired, bool actual)
|
|
{
|
|
switch (desired) {
|
|
case LAB_STATE_ENABLED:
|
|
return actual;
|
|
case LAB_STATE_DISABLED:
|
|
return !actual;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
query_str_match(const char *condition, const char *value)
|
|
{
|
|
if (!condition) {
|
|
return true;
|
|
}
|
|
return value && match_glob(condition, value);
|
|
}
|
|
|
|
bool
|
|
view_matches_query(struct view *view, struct view_query *query)
|
|
{
|
|
if (!query_str_match(query->identifier, view_get_string_prop(view, "app_id"))) {
|
|
return false;
|
|
}
|
|
|
|
if (!query_str_match(query->title, view_get_string_prop(view, "title"))) {
|
|
return false;
|
|
}
|
|
|
|
if (query->window_type >= 0 && !view_contains_window_type(view, query->window_type)) {
|
|
return false;
|
|
}
|
|
|
|
if (query->sandbox_engine || query->sandbox_app_id) {
|
|
const struct wlr_security_context_v1_state *ctx =
|
|
security_context_from_view(view);
|
|
|
|
if (!ctx) {
|
|
return false;
|
|
}
|
|
|
|
if (!query_str_match(query->sandbox_engine, ctx->sandbox_engine)) {
|
|
return false;
|
|
}
|
|
|
|
if (!query_str_match(query->sandbox_app_id, ctx->app_id)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!query_tristate_match(query->shaded, view->shaded)) {
|
|
return false;
|
|
}
|
|
|
|
if (query->maximized != VIEW_AXIS_INVALID && view->maximized != query->maximized) {
|
|
return false;
|
|
}
|
|
|
|
if (!query_tristate_match(query->iconified, view->minimized)) {
|
|
return false;
|
|
}
|
|
|
|
if (!query_tristate_match(query->focused, view->server->active_view == view)) {
|
|
return false;
|
|
}
|
|
|
|
if (!query_tristate_match(query->omnipresent, view->visible_on_all_workspaces)) {
|
|
return false;
|
|
}
|
|
|
|
if (query->tiled == LAB_EDGE_ANY) {
|
|
if (!view->tiled) {
|
|
return false;
|
|
}
|
|
} else if (query->tiled != LAB_EDGE_INVALID) {
|
|
if (query->tiled != view->tiled) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const char *tiled_region =
|
|
view->tiled_region ? view->tiled_region->name : NULL;
|
|
if (!query_str_match(query->tiled_region, tiled_region)) {
|
|
return false;
|
|
}
|
|
|
|
if (query->desktop) {
|
|
const char *view_workspace = view->workspace->name;
|
|
struct workspace *current = view->server->workspaces.current;
|
|
|
|
if (!strcasecmp(query->desktop, "other")) {
|
|
/* "other" means the view is NOT on the current desktop */
|
|
if (!strcasecmp(view_workspace, current->name)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
// TODO: perhaps wrap "left" and "right" workspaces
|
|
struct workspace *target =
|
|
workspaces_find(current, query->desktop, /* wrap */ false);
|
|
if (!target || strcasecmp(view_workspace, target->name)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (query->decoration != LAB_SSD_MODE_INVALID
|
|
&& query->decoration != view->ssd_mode) {
|
|
return false;
|
|
}
|
|
|
|
if (query->monitor) {
|
|
struct output *current = output_nearest_to_cursor(view->server);
|
|
|
|
if (!strcasecmp(query->monitor, "current") && current != view->output) {
|
|
return false;
|
|
}
|
|
if (!strcasecmp(query->monitor, "left") &&
|
|
output_get_adjacent(current, LAB_EDGE_LEFT, false) != view->output) {
|
|
return false;
|
|
}
|
|
if (!strcasecmp(query->monitor, "right") &&
|
|
output_get_adjacent(current, LAB_EDGE_RIGHT, false) != view->output) {
|
|
return false;
|
|
}
|
|
if (output_from_name(view->server, query->monitor) != view->output) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
matches_criteria(struct view *view, enum lab_view_criteria criteria)
|
|
{
|
|
if (!view_is_focusable(view)) {
|
|
return false;
|
|
}
|
|
if (criteria & LAB_VIEW_CRITERIA_CURRENT_WORKSPACE) {
|
|
/*
|
|
* Always-on-top views are always on the current desktop and are
|
|
* special in that they live in a different tree.
|
|
*/
|
|
struct server *server = view->server;
|
|
if (view->scene_tree->node.parent != server->workspaces.current->tree
|
|
&& !view_is_always_on_top(view)) {
|
|
return false;
|
|
}
|
|
}
|
|
if (criteria & LAB_VIEW_CRITERIA_FULLSCREEN) {
|
|
if (!view->fullscreen) {
|
|
return false;
|
|
}
|
|
}
|
|
if (criteria & LAB_VIEW_CRITERIA_ALWAYS_ON_TOP) {
|
|
if (!view_is_always_on_top(view)) {
|
|
return false;
|
|
}
|
|
}
|
|
if (criteria & LAB_VIEW_CRITERIA_ROOT_TOPLEVEL) {
|
|
if (view != view_get_root(view)) {
|
|
return false;
|
|
}
|
|
}
|
|
if (criteria & LAB_VIEW_CRITERIA_NO_ALWAYS_ON_TOP) {
|
|
if (view_is_always_on_top(view)) {
|
|
return false;
|
|
}
|
|
}
|
|
if (criteria & LAB_VIEW_CRITERIA_NO_SKIP_WINDOW_SWITCHER) {
|
|
if (window_rules_get_property(view, "skipWindowSwitcher") == LAB_PROP_TRUE) {
|
|
return false;
|
|
}
|
|
}
|
|
if (criteria & LAB_VIEW_CRITERIA_NO_OMNIPRESENT) {
|
|
/*
|
|
* TODO: Once always-on-top views use a per-workspace
|
|
* sub-tree we can remove the check from this condition.
|
|
*/
|
|
if (view->visible_on_all_workspaces || view_is_always_on_top(view)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
struct view *
|
|
view_next(struct wl_list *head, struct view *view, enum lab_view_criteria criteria)
|
|
{
|
|
assert(head);
|
|
|
|
struct wl_list *elm = view ? &view->link : head;
|
|
|
|
for (elm = elm->next; elm != head; elm = elm->next) {
|
|
view = wl_container_of(elm, view, link);
|
|
if (matches_criteria(view, criteria)) {
|
|
return view;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
struct view *
|
|
view_prev(struct wl_list *head, struct view *view, enum lab_view_criteria criteria)
|
|
{
|
|
assert(head);
|
|
|
|
struct wl_list *elm = view ? &view->link : head;
|
|
|
|
for (elm = elm->prev; elm != head; elm = elm->prev) {
|
|
view = wl_container_of(elm, view, link);
|
|
if (matches_criteria(view, criteria)) {
|
|
return view;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
struct view *
|
|
view_next_no_head_stop(struct wl_list *head, struct view *from,
|
|
enum lab_view_criteria criteria)
|
|
{
|
|
assert(head);
|
|
|
|
struct wl_list *elm = from ? &from->link : head;
|
|
|
|
struct wl_list *end = elm;
|
|
for (elm = elm->next; elm != end; elm = elm->next) {
|
|
if (elm == head) {
|
|
continue;
|
|
}
|
|
struct view *view = wl_container_of(elm, view, link);
|
|
if (matches_criteria(view, criteria)) {
|
|
return view;
|
|
}
|
|
}
|
|
return from;
|
|
}
|
|
|
|
struct view *
|
|
view_prev_no_head_stop(struct wl_list *head, struct view *from,
|
|
enum lab_view_criteria criteria)
|
|
{
|
|
assert(head);
|
|
|
|
struct wl_list *elm = from ? &from->link : head;
|
|
|
|
struct wl_list *end = elm;
|
|
for (elm = elm->prev; elm != end; elm = elm->prev) {
|
|
if (elm == head) {
|
|
continue;
|
|
}
|
|
struct view *view = wl_container_of(elm, view, link);
|
|
if (matches_criteria(view, criteria)) {
|
|
return view;
|
|
}
|
|
}
|
|
return from;
|
|
}
|
|
|
|
void
|
|
view_array_append(struct server *server, struct wl_array *views,
|
|
enum lab_view_criteria criteria)
|
|
{
|
|
struct view *view;
|
|
for_each_view(view, &server->views, criteria) {
|
|
struct view **entry = wl_array_add(views, sizeof(*entry));
|
|
if (!entry) {
|
|
wlr_log(WLR_ERROR, "wl_array_add(): out of memory");
|
|
continue;
|
|
}
|
|
*entry = view;
|
|
}
|
|
}
|
|
|
|
enum view_wants_focus
|
|
view_wants_focus(struct view *view)
|
|
{
|
|
assert(view);
|
|
if (view->impl->wants_focus) {
|
|
return view->impl->wants_focus(view);
|
|
}
|
|
return VIEW_WANTS_FOCUS_ALWAYS;
|
|
}
|
|
|
|
bool
|
|
view_contains_window_type(struct view *view, enum lab_window_type window_type)
|
|
{
|
|
assert(view);
|
|
if (view->impl->contains_window_type) {
|
|
return view->impl->contains_window_type(view, window_type);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
view_is_focusable(struct view *view)
|
|
{
|
|
assert(view);
|
|
if (!view->surface) {
|
|
return false;
|
|
}
|
|
|
|
switch (view_wants_focus(view)) {
|
|
case VIEW_WANTS_FOCUS_ALWAYS:
|
|
case VIEW_WANTS_FOCUS_LIKELY:
|
|
return (view->mapped || view->minimized);
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void
|
|
view_offer_focus(struct view *view)
|
|
{
|
|
assert(view);
|
|
if (view->impl->offer_focus) {
|
|
view->impl->offer_focus(view);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* All view_apply_xxx_geometry() functions must *not* modify
|
|
* any state besides repositioning or resizing the view.
|
|
*
|
|
* They may be called repeatably during output layout changes.
|
|
*/
|
|
|
|
struct wlr_box
|
|
view_get_edge_snap_box(struct view *view, struct output *output,
|
|
enum lab_edge edge)
|
|
{
|
|
struct wlr_box usable = output_usable_area_in_layout_coords(output);
|
|
int x1 = rc.gap;
|
|
int y1 = rc.gap;
|
|
int x2 = usable.width - rc.gap;
|
|
int y2 = usable.height - rc.gap;
|
|
|
|
if (edge & LAB_EDGE_RIGHT) {
|
|
x1 = (usable.width + rc.gap) / 2;
|
|
}
|
|
if (edge & LAB_EDGE_LEFT) {
|
|
x2 = (usable.width - rc.gap) / 2;
|
|
}
|
|
if (edge & LAB_EDGE_DOWN) {
|
|
y1 = (usable.height + rc.gap) / 2;
|
|
}
|
|
if (edge & LAB_EDGE_UP) {
|
|
y2 = (usable.height - rc.gap) / 2;
|
|
}
|
|
|
|
struct wlr_box dst = {
|
|
.x = x1 + usable.x,
|
|
.y = y1 + usable.y,
|
|
.width = x2 - x1,
|
|
.height = y2 - y1,
|
|
};
|
|
|
|
if (view) {
|
|
struct border margin = ssd_get_margin(view->ssd);
|
|
dst.x += margin.left;
|
|
dst.y += margin.top;
|
|
dst.width -= margin.left + margin.right;
|
|
dst.height -= margin.top + margin.bottom;
|
|
}
|
|
|
|
return dst;
|
|
}
|
|
|
|
static bool
|
|
view_discover_output(struct view *view, struct wlr_box *geometry)
|
|
{
|
|
assert(view);
|
|
|
|
if (!geometry) {
|
|
geometry = &view->current;
|
|
}
|
|
|
|
struct output *output =
|
|
output_nearest_to(view->server,
|
|
geometry->x + geometry->width / 2,
|
|
geometry->y + geometry->height / 2);
|
|
|
|
if (output && output != view->output) {
|
|
view->output = output;
|
|
/* Show fullscreen views above top-layer */
|
|
if (view->fullscreen) {
|
|
desktop_update_top_layer_visibility(view->server);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void
|
|
set_adaptive_sync_fullscreen(struct view *view)
|
|
{
|
|
if (!output_is_usable(view->output)) {
|
|
return;
|
|
}
|
|
if (rc.adaptive_sync != LAB_ADAPTIVE_SYNC_FULLSCREEN) {
|
|
return;
|
|
}
|
|
/* Enable adaptive sync if view is fullscreen */
|
|
output_enable_adaptive_sync(view->output, view->fullscreen);
|
|
output_state_commit(view->output);
|
|
}
|
|
|
|
void
|
|
view_set_activated(struct view *view, bool activated)
|
|
{
|
|
assert(view);
|
|
ssd_set_active(view->ssd, activated);
|
|
if (view->impl->set_activated) {
|
|
view->impl->set_activated(view, activated);
|
|
}
|
|
|
|
wl_signal_emit_mutable(&view->events.activated, &activated);
|
|
|
|
if (rc.kb_layout_per_window) {
|
|
if (!activated) {
|
|
/* Store configured keyboard layout per view */
|
|
view->keyboard_layout =
|
|
view->server->seat.keyboard_group->keyboard.modifiers.group;
|
|
} else {
|
|
/* Switch to previously stored keyboard layout */
|
|
keyboard_update_layout(&view->server->seat, view->keyboard_layout);
|
|
}
|
|
}
|
|
set_adaptive_sync_fullscreen(view);
|
|
}
|
|
|
|
void
|
|
view_set_output(struct view *view, struct output *output)
|
|
{
|
|
assert(view);
|
|
if (!output_is_usable(output)) {
|
|
wlr_log(WLR_ERROR, "invalid output set for view");
|
|
return;
|
|
}
|
|
view->output = output;
|
|
/* Show fullscreen views above top-layer */
|
|
if (view->fullscreen) {
|
|
desktop_update_top_layer_visibility(view->server);
|
|
}
|
|
}
|
|
|
|
void
|
|
view_close(struct view *view)
|
|
{
|
|
assert(view);
|
|
if (view->impl->close) {
|
|
view->impl->close(view);
|
|
}
|
|
}
|
|
|
|
static void
|
|
view_update_outputs(struct view *view)
|
|
{
|
|
struct output *output;
|
|
struct wlr_output_layout *layout = view->server->output_layout;
|
|
|
|
uint64_t new_outputs = 0;
|
|
wl_list_for_each(output, &view->server->outputs, link) {
|
|
if (output_is_usable(output) && wlr_output_layout_intersects(
|
|
layout, output->wlr_output, &view->current)) {
|
|
new_outputs |= (1ull << output->scene_output->WLR_PRIVATE.index);
|
|
}
|
|
}
|
|
|
|
if (new_outputs != view->outputs) {
|
|
view->outputs = new_outputs;
|
|
wl_signal_emit_mutable(&view->events.new_outputs, NULL);
|
|
desktop_update_top_layer_visibility(view->server);
|
|
}
|
|
}
|
|
|
|
bool
|
|
view_on_output(struct view *view, struct output *output)
|
|
{
|
|
assert(view);
|
|
assert(output);
|
|
return output->scene_output
|
|
&& (view->outputs & (1ull << output->scene_output->WLR_PRIVATE.index));
|
|
}
|
|
|
|
void
|
|
view_move(struct view *view, int x, int y)
|
|
{
|
|
assert(view);
|
|
view_move_resize(view, (struct wlr_box){
|
|
.x = x, .y = y,
|
|
.width = view->pending.width,
|
|
.height = view->pending.height
|
|
});
|
|
}
|
|
|
|
void
|
|
view_moved(struct view *view)
|
|
{
|
|
assert(view);
|
|
wlr_scene_node_set_position(&view->scene_tree->node,
|
|
view->current.x, view->current.y);
|
|
/*
|
|
* Only floating views change output when moved. Non-floating
|
|
* views (maximized/tiled/fullscreen) are tied to a particular
|
|
* output when they enter that state.
|
|
*/
|
|
if (view_is_floating(view)) {
|
|
view_discover_output(view, NULL);
|
|
}
|
|
view_update_outputs(view);
|
|
ssd_update_geometry(view->ssd);
|
|
cursor_update_focus(view->server);
|
|
if (rc.resize_indicator && view->server->grabbed_view == view) {
|
|
resize_indicator_update(view);
|
|
}
|
|
}
|
|
|
|
void
|
|
view_move_resize(struct view *view, struct wlr_box geo)
|
|
{
|
|
assert(view);
|
|
if (view->impl->configure) {
|
|
view->impl->configure(view, geo);
|
|
}
|
|
}
|
|
|
|
void
|
|
view_resize_relative(struct view *view, int left, int right, int top, int bottom)
|
|
{
|
|
assert(view);
|
|
if (view->fullscreen || view->maximized != VIEW_AXIS_NONE) {
|
|
return;
|
|
}
|
|
view_set_shade(view, false);
|
|
struct wlr_box newgeo = view->pending;
|
|
newgeo.x -= left;
|
|
newgeo.width += left + right;
|
|
newgeo.y -= top;
|
|
newgeo.height += top + bottom;
|
|
view_move_resize(view, newgeo);
|
|
view_set_untiled(view);
|
|
}
|
|
|
|
void
|
|
view_move_relative(struct view *view, int x, int y)
|
|
{
|
|
assert(view);
|
|
if (view->fullscreen) {
|
|
return;
|
|
}
|
|
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);
|
|
}
|
|
view_move(view, view->pending.x + x, view->pending.y + y);
|
|
}
|
|
|
|
void
|
|
view_move_to_cursor(struct view *view)
|
|
{
|
|
assert(view);
|
|
|
|
struct output *pending_output = output_nearest_to_cursor(view->server);
|
|
if (!output_is_usable(pending_output)) {
|
|
return;
|
|
}
|
|
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);
|
|
}
|
|
|
|
struct border margin = ssd_thickness(view);
|
|
struct wlr_box geo = view->pending;
|
|
geo.width += margin.left + margin.right;
|
|
geo.height += margin.top + margin.bottom;
|
|
|
|
int x = view->server->seat.cursor->x - (geo.width / 2);
|
|
int y = view->server->seat.cursor->y - (geo.height / 2);
|
|
|
|
struct wlr_box usable = output_usable_area_in_layout_coords(pending_output);
|
|
|
|
/* Limit usable region to account for gap */
|
|
usable.x += rc.gap;
|
|
usable.y += rc.gap;
|
|
usable.width -= 2 * rc.gap;
|
|
usable.height -= 2 * rc.gap;
|
|
|
|
if (x + geo.width > usable.x + usable.width) {
|
|
x = usable.x + usable.width - geo.width;
|
|
}
|
|
x = MAX(x, usable.x) + margin.left;
|
|
|
|
if (y + geo.height > usable.y + usable.height) {
|
|
y = usable.y + usable.height - geo.height;
|
|
}
|
|
y = MAX(y, usable.y) + margin.top;
|
|
|
|
view_move(view, x, y);
|
|
}
|
|
|
|
struct view_size_hints
|
|
view_get_size_hints(struct view *view)
|
|
{
|
|
assert(view);
|
|
if (view->impl->get_size_hints) {
|
|
return view->impl->get_size_hints(view);
|
|
}
|
|
return (struct view_size_hints){0};
|
|
}
|
|
|
|
static void
|
|
substitute_nonzero(int *a, int *b)
|
|
{
|
|
if (!(*a)) {
|
|
*a = *b;
|
|
} else if (!(*b)) {
|
|
*b = *a;
|
|
}
|
|
}
|
|
|
|
static int
|
|
round_to_increment(int val, int base, int inc)
|
|
{
|
|
if (base < 0 || inc <= 0) {
|
|
return val;
|
|
}
|
|
return base + (val - base + inc / 2) / inc * inc;
|
|
}
|
|
|
|
void
|
|
view_adjust_size(struct view *view, int *w, int *h)
|
|
{
|
|
assert(view);
|
|
struct view_size_hints hints = view_get_size_hints(view);
|
|
int min_width = view_get_min_width();
|
|
|
|
/*
|
|
* "If a base size is not provided, the minimum size is to be
|
|
* used in its place and vice versa." (ICCCM 4.1.2.3)
|
|
*/
|
|
substitute_nonzero(&hints.min_width, &hints.base_width);
|
|
substitute_nonzero(&hints.min_height, &hints.base_height);
|
|
|
|
/*
|
|
* Snap width/height to requested size increments (if any).
|
|
* Typically, terminal emulators use these to make sure that the
|
|
* terminal is resized to a width/height evenly divisible by the
|
|
* cell (character) size.
|
|
*/
|
|
*w = round_to_increment(*w, hints.base_width, hints.width_inc);
|
|
*h = round_to_increment(*h, hints.base_height, hints.height_inc);
|
|
|
|
/*
|
|
* If a minimum width/height was not set, then use default.
|
|
* This is currently always the case for xdg-shell views.
|
|
*/
|
|
if (hints.min_width < 1) {
|
|
hints.min_width = min_width;
|
|
}
|
|
if (hints.min_height < 1) {
|
|
hints.min_height = LAB_MIN_VIEW_HEIGHT;
|
|
}
|
|
*w = MAX(*w, hints.min_width);
|
|
*h = MAX(*h, hints.min_height);
|
|
}
|
|
|
|
static void
|
|
_minimize(struct view *view, bool minimized)
|
|
{
|
|
assert(view);
|
|
if (view->minimized == minimized) {
|
|
return;
|
|
}
|
|
|
|
if (view->impl->minimize) {
|
|
view->impl->minimize(view, minimized);
|
|
}
|
|
|
|
view->minimized = minimized;
|
|
wl_signal_emit_mutable(&view->events.minimized, NULL);
|
|
|
|
if (minimized) {
|
|
view->impl->unmap(view, /* client_request */ false);
|
|
} else {
|
|
view->impl->map(view);
|
|
}
|
|
}
|
|
|
|
static void
|
|
minimize_sub_views(struct view *view, bool minimized)
|
|
{
|
|
struct view **child;
|
|
struct wl_array children;
|
|
|
|
wl_array_init(&children);
|
|
view_append_children(view, &children);
|
|
wl_array_for_each(child, &children) {
|
|
_minimize(*child, minimized);
|
|
minimize_sub_views(*child, minimized);
|
|
}
|
|
wl_array_release(&children);
|
|
}
|
|
|
|
/*
|
|
* Minimize the whole view-hierarchy from top to bottom regardless of which one
|
|
* in the hierarchy requested the minimize. For example, if an 'About' or
|
|
* 'Open File' dialog is minimized, its toplevel is minimized also. And vice
|
|
* versa.
|
|
*/
|
|
void
|
|
view_minimize(struct view *view, bool minimized)
|
|
{
|
|
assert(view);
|
|
|
|
if (view->server->input_mode == LAB_INPUT_STATE_WINDOW_SWITCHER) {
|
|
wlr_log(WLR_ERROR, "not minimizing window while window switching");
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Minimize the root window first because some xwayland clients send a
|
|
* request-unmap to sub-windows at this point (for example gimp and its
|
|
* 'open file' dialog), so it saves trying to unmap them twice
|
|
*/
|
|
struct view *root = view_get_root(view);
|
|
_minimize(root, minimized);
|
|
minimize_sub_views(root, minimized);
|
|
|
|
/* Enable top-layer when full-screen views are minimized */
|
|
if (view->fullscreen && view->output) {
|
|
desktop_update_top_layer_visibility(view->server);
|
|
}
|
|
}
|
|
|
|
bool
|
|
view_compute_centered_position(struct view *view, const struct wlr_box *ref,
|
|
int w, int h, int *x, int *y)
|
|
{
|
|
assert(view);
|
|
if (w <= 0 || h <= 0) {
|
|
wlr_log(WLR_ERROR, "view has empty geometry, not centering");
|
|
return false;
|
|
}
|
|
if (!output_is_usable(view->output)) {
|
|
wlr_log(WLR_ERROR, "view has no output, not centering");
|
|
return false;
|
|
}
|
|
|
|
struct border margin = ssd_get_margin(view->ssd);
|
|
struct wlr_box usable = output_usable_area_in_layout_coords(view->output);
|
|
int width = w + margin.left + margin.right;
|
|
int height = h + margin.top + margin.bottom;
|
|
|
|
/* If reference box is NULL then center to usable area */
|
|
if (!ref) {
|
|
ref = &usable;
|
|
}
|
|
*x = ref->x + (ref->width - width) / 2;
|
|
*y = ref->y + (ref->height - height) / 2;
|
|
|
|
/* Fit the view within the usable area */
|
|
if (*x < usable.x) {
|
|
*x = usable.x;
|
|
} else if (*x + width > usable.x + usable.width) {
|
|
*x = usable.x + usable.width - width;
|
|
}
|
|
if (*y < usable.y) {
|
|
*y = usable.y;
|
|
} else if (*y + height > usable.y + usable.height) {
|
|
*y = usable.y + usable.height - height;
|
|
}
|
|
|
|
*x += margin.left;
|
|
*y += margin.top;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
adjust_floating_geometry(struct view *view, struct wlr_box *geometry,
|
|
bool midpoint_visibility)
|
|
{
|
|
assert(view);
|
|
|
|
if (!output_is_usable(view->output)) {
|
|
wlr_log(WLR_ERROR, "view has no output, not positioning");
|
|
return false;
|
|
}
|
|
|
|
/* Avoid moving panels out of their own reserved area ("strut") */
|
|
if (window_rules_get_property(view, "fixedPosition") == LAB_PROP_TRUE
|
|
|| view_has_strut_partial(view)) {
|
|
return false;
|
|
}
|
|
|
|
bool adjusted = false;
|
|
bool onscreen = false;
|
|
if (wlr_output_layout_intersects(view->server->output_layout,
|
|
view->output->wlr_output, geometry)) {
|
|
/* Always make sure the titlebar starts within the usable area */
|
|
struct border margin = ssd_get_margin(view->ssd);
|
|
struct wlr_box usable =
|
|
output_usable_area_in_layout_coords(view->output);
|
|
|
|
if (geometry->x < usable.x + margin.left) {
|
|
geometry->x = usable.x + margin.left;
|
|
adjusted = true;
|
|
}
|
|
|
|
if (geometry->y < usable.y + margin.top) {
|
|
geometry->y = usable.y + margin.top;
|
|
adjusted = true;
|
|
}
|
|
|
|
if (!midpoint_visibility) {
|
|
/*
|
|
* If midpoint visibility is not required, the view is
|
|
* on screen if at least one pixel is visible.
|
|
*/
|
|
onscreen = true;
|
|
} else {
|
|
/* Otherwise, make sure the midpoint is on screen */
|
|
int mx = geometry->x + geometry->width / 2;
|
|
int my = geometry->y + geometry->height / 2;
|
|
|
|
onscreen = mx <= usable.x + usable.width &&
|
|
my <= usable.y + usable.height;
|
|
}
|
|
}
|
|
|
|
if (onscreen) {
|
|
return adjusted;
|
|
}
|
|
|
|
/* Reposition offscreen automatically if configured to do so */
|
|
if (rc.placement_policy == LAB_PLACE_AUTOMATIC) {
|
|
if (placement_find_best(view, geometry)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/* If automatic placement failed or was not enabled, just center */
|
|
return view_compute_centered_position(view, NULL,
|
|
geometry->width, geometry->height,
|
|
&geometry->x, &geometry->y);
|
|
}
|
|
|
|
void
|
|
view_set_fallback_natural_geometry(struct view *view)
|
|
{
|
|
view->natural_geometry.width = VIEW_FALLBACK_WIDTH;
|
|
view->natural_geometry.height = VIEW_FALLBACK_HEIGHT;
|
|
view_compute_centered_position(view, NULL,
|
|
view->natural_geometry.width,
|
|
view->natural_geometry.height,
|
|
&view->natural_geometry.x,
|
|
&view->natural_geometry.y);
|
|
}
|
|
|
|
void
|
|
view_store_natural_geometry(struct view *view)
|
|
{
|
|
assert(view);
|
|
/*
|
|
* Do not overwrite the stored geometry if fullscreen or tiled.
|
|
* Maximized views are handled on a per-axis basis (see below).
|
|
*/
|
|
if (view->fullscreen || view_is_tiled(view)) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Note that for xdg-shell views that start fullscreen or maximized,
|
|
* we end up storing a natural geometry of 0x0. This is intentional.
|
|
* When leaving fullscreen or unmaximizing, we pass 0x0 to the
|
|
* xdg-toplevel configure event, which means the application should
|
|
* choose its own size.
|
|
*/
|
|
if (!(view->maximized & VIEW_AXIS_HORIZONTAL)) {
|
|
view->natural_geometry.x = view->pending.x;
|
|
view->natural_geometry.width = view->pending.width;
|
|
}
|
|
if (!(view->maximized & VIEW_AXIS_VERTICAL)) {
|
|
view->natural_geometry.y = view->pending.y;
|
|
view->natural_geometry.height = view->pending.height;
|
|
}
|
|
}
|
|
|
|
int
|
|
view_effective_height(struct view *view, bool use_pending)
|
|
{
|
|
assert(view);
|
|
|
|
if (view->shaded) {
|
|
return 0;
|
|
}
|
|
|
|
return use_pending ? view->pending.height : view->current.height;
|
|
}
|
|
|
|
void
|
|
view_center(struct view *view, const struct wlr_box *ref)
|
|
{
|
|
assert(view);
|
|
int x, y;
|
|
if (view_compute_centered_position(view, ref, view->pending.width,
|
|
view->pending.height, &x, &y)) {
|
|
view_move(view, x, y);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Algorithm based on KWin's implementation:
|
|
* https://github.com/KDE/kwin/blob/df9f8f8346b5b7645578e37365dabb1a7b02ca5a/src/placement.cpp#L589
|
|
*/
|
|
static void
|
|
view_cascade(struct view *view)
|
|
{
|
|
/* "cascade" policy places a new view at center by default */
|
|
struct wlr_box center = view->pending;
|
|
view_compute_centered_position(view, NULL,
|
|
center.width, center.height, ¢er.x, ¢er.y);
|
|
struct border margin = ssd_get_margin(view->ssd);
|
|
center.x -= margin.left;
|
|
center.y -= margin.top;
|
|
center.width += margin.left + margin.right;
|
|
center.height += margin.top + margin.bottom;
|
|
|
|
/* Candidate geometry to which the view is moved */
|
|
struct wlr_box candidate = center;
|
|
|
|
struct wlr_box usable = output_usable_area_in_layout_coords(view->output);
|
|
|
|
/* TODO: move this logic to rcxml.c */
|
|
int offset_x = rc.placement_cascade_offset_x;
|
|
int offset_y = rc.placement_cascade_offset_y;
|
|
struct theme *theme = view->server->theme;
|
|
int default_offset = theme->titlebar_height + theme->border_width + 5;
|
|
if (offset_x <= 0) {
|
|
offset_x = default_offset;
|
|
}
|
|
if (offset_y <= 0) {
|
|
offset_y = default_offset;
|
|
}
|
|
|
|
/*
|
|
* Keep updating the candidate until it doesn't cover any existing views
|
|
* or doesn't fit within the usable area.
|
|
*/
|
|
bool candidate_updated = true;
|
|
while (candidate_updated) {
|
|
candidate_updated = false;
|
|
struct wlr_box covered = {0};
|
|
|
|
/* Iterate over views from top to bottom */
|
|
struct view *other_view;
|
|
for_each_view(other_view, &view->server->views,
|
|
LAB_VIEW_CRITERIA_CURRENT_WORKSPACE) {
|
|
struct wlr_box other = ssd_max_extents(other_view);
|
|
if (other_view == view
|
|
|| view->minimized
|
|
|| !box_intersects(&candidate, &other)) {
|
|
continue;
|
|
}
|
|
/*
|
|
* If the candidate covers an existing view whose
|
|
* top-left corner is not covered by other views,
|
|
* shift the candidate to bottom-right.
|
|
*/
|
|
if (wlr_box_contains_box(&candidate, &other)
|
|
&& !wlr_box_contains_point(
|
|
&covered, other.x, other.y)) {
|
|
candidate.x = other.x + offset_x;
|
|
candidate.y = other.y + offset_y;
|
|
if (!wlr_box_contains_box(&usable, &candidate)) {
|
|
/*
|
|
* If the candidate doesn't fit within
|
|
* the usable area, fall back to center
|
|
* and finish updating the candidate.
|
|
*/
|
|
candidate = center;
|
|
break;
|
|
} else {
|
|
/* Repeat with the new candidate */
|
|
candidate_updated = true;
|
|
break;
|
|
}
|
|
}
|
|
/*
|
|
* We use just a bounding box to represent the covered
|
|
* area, which would be fine for our use-case.
|
|
*/
|
|
box_union(&covered, &covered, &other);
|
|
}
|
|
}
|
|
|
|
view_move(view, candidate.x + margin.left, candidate.y + margin.top);
|
|
}
|
|
|
|
void
|
|
view_place_by_policy(struct view *view, bool allow_cursor,
|
|
enum lab_placement_policy policy)
|
|
{
|
|
if (allow_cursor && policy == LAB_PLACE_CURSOR) {
|
|
view_move_to_cursor(view);
|
|
return;
|
|
} else if (policy == LAB_PLACE_AUTOMATIC) {
|
|
struct wlr_box geometry = view->pending;
|
|
if (placement_find_best(view, &geometry)) {
|
|
view_move(view, geometry.x, geometry.y);
|
|
return;
|
|
}
|
|
} else if (policy == LAB_PLACE_CASCADE) {
|
|
view_cascade(view);
|
|
return;
|
|
}
|
|
|
|
view_center(view, NULL);
|
|
}
|
|
|
|
void
|
|
view_constrain_size_to_that_of_usable_area(struct view *view)
|
|
{
|
|
if (!view || !view->output || view->fullscreen) {
|
|
return;
|
|
}
|
|
|
|
struct wlr_box usable_area =
|
|
output_usable_area_in_layout_coords(view->output);
|
|
struct border margin = ssd_get_margin(view->ssd);
|
|
|
|
int available_width = usable_area.width - margin.left - margin.right;
|
|
int available_height = usable_area.height - margin.top - margin.bottom;
|
|
|
|
if (available_width <= 0 || available_height <= 0) {
|
|
return;
|
|
}
|
|
|
|
if (available_height >= view->pending.height &&
|
|
available_width >= view->pending.width) {
|
|
return;
|
|
}
|
|
|
|
int width = MIN(view->pending.width, available_width);
|
|
int height = MIN(view->pending.height, available_height);
|
|
|
|
int right_edge = usable_area.x + usable_area.width;
|
|
int bottom_edge = usable_area.y + usable_area.height;
|
|
|
|
int x =
|
|
MAX(usable_area.x + margin.left,
|
|
MIN(view->pending.x, right_edge - width - margin.right));
|
|
|
|
int y =
|
|
MAX(usable_area.y + margin.top,
|
|
MIN(view->pending.y, bottom_edge - height - margin.bottom));
|
|
|
|
struct wlr_box box = {
|
|
.x = x,
|
|
.y = y,
|
|
.width = width,
|
|
.height = height,
|
|
};
|
|
view_move_resize(view, box);
|
|
}
|
|
|
|
void
|
|
view_apply_natural_geometry(struct view *view)
|
|
{
|
|
assert(view);
|
|
assert(view_is_floating(view));
|
|
|
|
struct wlr_box geometry = view->natural_geometry;
|
|
/* Only adjust natural geometry if known (not 0x0) */
|
|
if (!wlr_box_empty(&geometry)) {
|
|
adjust_floating_geometry(view, &geometry,
|
|
/* midpoint_visibility */ false);
|
|
}
|
|
view_move_resize(view, geometry);
|
|
}
|
|
|
|
struct wlr_box
|
|
view_get_region_snap_box(struct view *view, struct region *region)
|
|
{
|
|
struct wlr_box geo = region->geo;
|
|
|
|
/* Adjust for rc.gap */
|
|
if (rc.gap) {
|
|
double half_gap = rc.gap / 2.0;
|
|
struct wlr_fbox offset = {
|
|
.x = half_gap,
|
|
.y = half_gap,
|
|
.width = -rc.gap,
|
|
.height = -rc.gap
|
|
};
|
|
struct wlr_box usable =
|
|
output_usable_area_in_layout_coords(region->output);
|
|
if (geo.x == usable.x) {
|
|
offset.x += half_gap;
|
|
offset.width -= half_gap;
|
|
}
|
|
if (geo.y == usable.y) {
|
|
offset.y += half_gap;
|
|
offset.height -= half_gap;
|
|
}
|
|
if (geo.x + geo.width == usable.x + usable.width) {
|
|
offset.width -= half_gap;
|
|
}
|
|
if (geo.y + geo.height == usable.y + usable.height) {
|
|
offset.height -= half_gap;
|
|
}
|
|
geo.x += offset.x;
|
|
geo.y += offset.y;
|
|
geo.width += offset.width;
|
|
geo.height += offset.height;
|
|
}
|
|
|
|
/* And adjust for current view */
|
|
if (view) {
|
|
struct border margin = ssd_get_margin(view->ssd);
|
|
geo.x += margin.left;
|
|
geo.y += margin.top;
|
|
geo.width -= margin.left + margin.right;
|
|
geo.height -= margin.top + margin.bottom;
|
|
}
|
|
|
|
return geo;
|
|
}
|
|
|
|
static void
|
|
view_apply_region_geometry(struct view *view)
|
|
{
|
|
assert(view);
|
|
assert(view->tiled_region || view->tiled_region_evacuate);
|
|
struct output *output = view->output;
|
|
assert(output_is_usable(output));
|
|
|
|
if (view->tiled_region_evacuate) {
|
|
/* View was evacuated from a destroying output */
|
|
/* Get new output local region, may be NULL */
|
|
view->tiled_region = regions_from_name(
|
|
view->tiled_region_evacuate, output);
|
|
|
|
/* Get rid of the evacuate instruction */
|
|
zfree(view->tiled_region_evacuate);
|
|
|
|
if (!view->tiled_region) {
|
|
/* Existing region name doesn't exist in rc.xml anymore */
|
|
view_set_untiled(view);
|
|
view_apply_natural_geometry(view);
|
|
return;
|
|
}
|
|
}
|
|
|
|
struct wlr_box geo = view_get_region_snap_box(view, view->tiled_region);
|
|
view_move_resize(view, geo);
|
|
}
|
|
|
|
static void
|
|
view_apply_tiled_geometry(struct view *view)
|
|
{
|
|
assert(view);
|
|
assert(view->tiled);
|
|
assert(output_is_usable(view->output));
|
|
|
|
view_move_resize(view, view_get_edge_snap_box(view,
|
|
view->output, view->tiled));
|
|
}
|
|
|
|
static void
|
|
view_apply_fullscreen_geometry(struct view *view)
|
|
{
|
|
assert(view);
|
|
assert(view->fullscreen);
|
|
assert(output_is_usable(view->output));
|
|
|
|
struct wlr_box box = { 0 };
|
|
wlr_output_effective_resolution(view->output->wlr_output,
|
|
&box.width, &box.height);
|
|
double ox = 0, oy = 0;
|
|
wlr_output_layout_output_coords(view->server->output_layout,
|
|
view->output->wlr_output, &ox, &oy);
|
|
box.x -= ox;
|
|
box.y -= oy;
|
|
view_move_resize(view, box);
|
|
}
|
|
|
|
static void
|
|
view_apply_maximized_geometry(struct view *view)
|
|
{
|
|
assert(view);
|
|
assert(view->maximized != VIEW_AXIS_NONE);
|
|
struct output *output = view->output;
|
|
assert(output_is_usable(output));
|
|
|
|
struct wlr_box box = output_usable_area_in_layout_coords(output);
|
|
if (box.height == output->wlr_output->height
|
|
&& output->wlr_output->scale != 1) {
|
|
box.height /= output->wlr_output->scale;
|
|
}
|
|
if (box.width == output->wlr_output->width
|
|
&& output->wlr_output->scale != 1) {
|
|
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
|
|
&& !box_intersects(&box, &natural)) {
|
|
view_compute_centered_position(view, NULL,
|
|
natural.width, natural.height,
|
|
&natural.x, &natural.y);
|
|
}
|
|
|
|
if (view->ssd_mode) {
|
|
struct border border = ssd_thickness(view);
|
|
box.x += border.left;
|
|
box.y += border.top;
|
|
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);
|
|
}
|
|
|
|
static void
|
|
view_apply_special_geometry(struct view *view)
|
|
{
|
|
assert(view);
|
|
assert(!view_is_floating(view));
|
|
if (!output_is_usable(view->output)) {
|
|
wlr_log(WLR_ERROR, "view has no output, not updating geometry");
|
|
return;
|
|
}
|
|
|
|
if (view->fullscreen) {
|
|
view_apply_fullscreen_geometry(view);
|
|
} else if (view->maximized != VIEW_AXIS_NONE) {
|
|
view_apply_maximized_geometry(view);
|
|
} else if (view->tiled) {
|
|
view_apply_tiled_geometry(view);
|
|
} else if (view->tiled_region || view->tiled_region_evacuate) {
|
|
view_apply_region_geometry(view);
|
|
} else {
|
|
assert(false); // not reached
|
|
}
|
|
}
|
|
|
|
/* For internal use only. Does not update geometry. */
|
|
static void
|
|
set_maximized(struct view *view, enum view_axis maximized)
|
|
{
|
|
if (view->impl->maximize) {
|
|
view->impl->maximize(view, maximized);
|
|
}
|
|
|
|
view->maximized = maximized;
|
|
wl_signal_emit_mutable(&view->events.maximized, NULL);
|
|
|
|
/*
|
|
* Ensure that follow-up actions like SnapToEdge / SnapToRegion
|
|
* use up-to-date SSD margin information. Otherwise we will end
|
|
* up using an outdated ssd->margin to calculate offsets.
|
|
*/
|
|
ssd_update_margin(view->ssd);
|
|
}
|
|
|
|
/*
|
|
* Un-maximize view and move it to specific geometry. Does not reset
|
|
* tiled state (use view_set_untiled() if you want that).
|
|
*/
|
|
void
|
|
view_restore_to(struct view *view, struct wlr_box geometry)
|
|
{
|
|
assert(view);
|
|
if (view->fullscreen) {
|
|
return;
|
|
}
|
|
if (view->maximized != VIEW_AXIS_NONE) {
|
|
set_maximized(view, VIEW_AXIS_NONE);
|
|
}
|
|
view_move_resize(view, geometry);
|
|
}
|
|
|
|
bool
|
|
view_is_tiled(struct view *view)
|
|
{
|
|
assert(view);
|
|
return (view->tiled || view->tiled_region
|
|
|| view->tiled_region_evacuate);
|
|
}
|
|
|
|
bool
|
|
view_is_tiled_and_notify_tiled(struct view *view)
|
|
{
|
|
switch (rc.snap_tiling_events_mode) {
|
|
case LAB_TILING_EVENTS_NEVER:
|
|
return false;
|
|
case LAB_TILING_EVENTS_REGION:
|
|
return view->tiled_region || view->tiled_region_evacuate;
|
|
case LAB_TILING_EVENTS_EDGE:
|
|
return view->tiled;
|
|
case LAB_TILING_EVENTS_ALWAYS:
|
|
return view_is_tiled(view);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
view_is_floating(struct view *view)
|
|
{
|
|
assert(view);
|
|
return !(view->fullscreen || (view->maximized != VIEW_AXIS_NONE)
|
|
|| view_is_tiled(view));
|
|
}
|
|
|
|
static void
|
|
view_notify_tiled(struct view *view)
|
|
{
|
|
assert(view);
|
|
if (view->impl->notify_tiled) {
|
|
view->impl->notify_tiled(view);
|
|
}
|
|
}
|
|
|
|
/* Reset tiled state of view without changing geometry */
|
|
void
|
|
view_set_untiled(struct view *view)
|
|
{
|
|
assert(view);
|
|
view->tiled = LAB_EDGE_INVALID;
|
|
view->tiled_region = NULL;
|
|
zfree(view->tiled_region_evacuate);
|
|
view_notify_tiled(view);
|
|
}
|
|
|
|
void
|
|
view_maximize(struct view *view, enum view_axis axis,
|
|
bool store_natural_geometry)
|
|
{
|
|
assert(view);
|
|
|
|
if (view->maximized == axis) {
|
|
return;
|
|
}
|
|
|
|
if (view->fullscreen) {
|
|
return;
|
|
}
|
|
|
|
view_set_shade(view, false);
|
|
|
|
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 && view_is_floating(view)) {
|
|
view_invalidate_last_layout_geometry(view);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Update natural geometry for any axis that wasn't already
|
|
* maximized. This is needed even when unmaximizing, because in
|
|
* single-axis cases the client may have resized the other axis
|
|
* while one axis was maximized.
|
|
*/
|
|
if (store_natural_geometry) {
|
|
view_store_natural_geometry(view);
|
|
}
|
|
|
|
/*
|
|
* When natural geometry is unknown (0x0) for an xdg-shell view,
|
|
* we normally send a configure event of 0x0 to get the client's
|
|
* preferred size, but this doesn't work if unmaximizing only
|
|
* one axis. So in that corner case, set a fallback geometry.
|
|
*/
|
|
if ((axis == VIEW_AXIS_HORIZONTAL || axis == VIEW_AXIS_VERTICAL)
|
|
&& wlr_box_empty(&view->natural_geometry)) {
|
|
view_set_fallback_natural_geometry(view);
|
|
}
|
|
|
|
set_maximized(view, axis);
|
|
if (view_is_floating(view)) {
|
|
view_apply_natural_geometry(view);
|
|
} else {
|
|
view_apply_special_geometry(view);
|
|
}
|
|
}
|
|
|
|
void
|
|
view_toggle_maximize(struct view *view, enum view_axis axis)
|
|
{
|
|
assert(view);
|
|
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;
|
|
}
|
|
}
|
|
|
|
bool
|
|
view_wants_decorations(struct view *view)
|
|
{
|
|
/* Window-rules take priority if they exist for this view */
|
|
switch (window_rules_get_property(view, "serverDecoration")) {
|
|
case LAB_PROP_TRUE:
|
|
return true;
|
|
case LAB_PROP_FALSE:
|
|
return false;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* view->ssd_preference may be set by the decoration implementation
|
|
* e.g. src/decorations/xdg-deco.c or src/decorations/kde-deco.c.
|
|
*/
|
|
switch (view->ssd_preference) {
|
|
case LAB_SSD_PREF_SERVER:
|
|
return true;
|
|
case LAB_SSD_PREF_CLIENT:
|
|
return false;
|
|
default:
|
|
/*
|
|
* We don't know anything about the client preference
|
|
* so fall back to core.decoration settings in rc.xml
|
|
*/
|
|
return rc.xdg_shell_server_side_deco;
|
|
}
|
|
}
|
|
|
|
void
|
|
view_set_decorations(struct view *view, enum lab_ssd_mode mode, bool force_ssd)
|
|
{
|
|
assert(view);
|
|
|
|
if (force_ssd || view_wants_decorations(view)
|
|
|| mode < view->ssd_mode) {
|
|
view_set_ssd_mode(view, mode);
|
|
}
|
|
}
|
|
|
|
void
|
|
view_toggle_decorations(struct view *view)
|
|
{
|
|
assert(view);
|
|
|
|
if (rc.ssd_keep_border && view->ssd_mode == LAB_SSD_MODE_FULL) {
|
|
view_set_ssd_mode(view, LAB_SSD_MODE_BORDER);
|
|
} else if (view->ssd_mode != LAB_SSD_MODE_NONE) {
|
|
view_set_ssd_mode(view, LAB_SSD_MODE_NONE);
|
|
} else {
|
|
view_set_ssd_mode(view, LAB_SSD_MODE_FULL);
|
|
}
|
|
}
|
|
|
|
bool
|
|
view_is_always_on_top(struct view *view)
|
|
{
|
|
assert(view);
|
|
return view->scene_tree->node.parent ==
|
|
view->server->view_tree_always_on_top;
|
|
}
|
|
|
|
void
|
|
view_toggle_always_on_top(struct view *view)
|
|
{
|
|
assert(view);
|
|
if (view_is_always_on_top(view)) {
|
|
view->workspace = view->server->workspaces.current;
|
|
wlr_scene_node_reparent(&view->scene_tree->node,
|
|
view->workspace->tree);
|
|
} else {
|
|
wlr_scene_node_reparent(&view->scene_tree->node,
|
|
view->server->view_tree_always_on_top);
|
|
}
|
|
}
|
|
|
|
bool
|
|
view_is_always_on_bottom(struct view *view)
|
|
{
|
|
assert(view);
|
|
return view->scene_tree->node.parent ==
|
|
view->server->view_tree_always_on_bottom;
|
|
}
|
|
|
|
void
|
|
view_toggle_always_on_bottom(struct view *view)
|
|
{
|
|
assert(view);
|
|
if (view_is_always_on_bottom(view)) {
|
|
view->workspace = view->server->workspaces.current;
|
|
wlr_scene_node_reparent(&view->scene_tree->node,
|
|
view->workspace->tree);
|
|
} else {
|
|
wlr_scene_node_reparent(&view->scene_tree->node,
|
|
view->server->view_tree_always_on_bottom);
|
|
}
|
|
}
|
|
|
|
void
|
|
view_toggle_visible_on_all_workspaces(struct view *view)
|
|
{
|
|
assert(view);
|
|
view->visible_on_all_workspaces = !view->visible_on_all_workspaces;
|
|
ssd_update_geometry(view->ssd);
|
|
}
|
|
|
|
void
|
|
view_move_to_workspace(struct view *view, struct workspace *workspace)
|
|
{
|
|
assert(view);
|
|
assert(workspace);
|
|
if (view->workspace != workspace) {
|
|
view->workspace = workspace;
|
|
wlr_scene_node_reparent(&view->scene_tree->node,
|
|
workspace->tree);
|
|
}
|
|
}
|
|
|
|
static void
|
|
decorate(struct view *view)
|
|
{
|
|
if (!view->ssd) {
|
|
view->ssd = ssd_create(view,
|
|
view == view->server->active_view);
|
|
}
|
|
}
|
|
|
|
static void
|
|
undecorate(struct view *view)
|
|
{
|
|
ssd_destroy(view->ssd);
|
|
view->ssd = NULL;
|
|
}
|
|
|
|
bool
|
|
view_titlebar_visible(struct view *view)
|
|
{
|
|
if (view->maximized == VIEW_AXIS_BOTH
|
|
&& rc.hide_maximized_window_titlebar) {
|
|
return false;
|
|
}
|
|
return view->ssd_mode == LAB_SSD_MODE_FULL;
|
|
}
|
|
|
|
void
|
|
view_set_ssd_mode(struct view *view, enum lab_ssd_mode mode)
|
|
{
|
|
assert(view);
|
|
|
|
if (view->shaded || view->fullscreen
|
|
|| mode == view->ssd_mode) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Set these first since they are referenced
|
|
* within the call tree of ssd_create() and ssd_thickness()
|
|
*/
|
|
view->ssd_mode = mode;
|
|
|
|
if (mode) {
|
|
decorate(view);
|
|
ssd_set_titlebar(view->ssd, view_titlebar_visible(view));
|
|
} else {
|
|
undecorate(view);
|
|
}
|
|
|
|
if (!view_is_floating(view)) {
|
|
view_apply_special_geometry(view);
|
|
}
|
|
}
|
|
|
|
void
|
|
view_toggle_fullscreen(struct view *view)
|
|
{
|
|
assert(view);
|
|
|
|
view_set_fullscreen(view, !view->fullscreen);
|
|
}
|
|
|
|
/* For internal use only. Does not update geometry. */
|
|
static void
|
|
set_fullscreen(struct view *view, bool fullscreen)
|
|
{
|
|
/* When going fullscreen, unshade the window */
|
|
if (fullscreen) {
|
|
view_set_shade(view, false);
|
|
}
|
|
|
|
/* Hide decorations when going fullscreen */
|
|
if (fullscreen && view->ssd_mode) {
|
|
undecorate(view);
|
|
}
|
|
|
|
if (view->impl->set_fullscreen) {
|
|
view->impl->set_fullscreen(view, fullscreen);
|
|
}
|
|
|
|
view->fullscreen = fullscreen;
|
|
wl_signal_emit_mutable(&view->events.fullscreened, NULL);
|
|
|
|
/* Re-show decorations when no longer fullscreen */
|
|
if (!fullscreen && view->ssd_mode) {
|
|
decorate(view);
|
|
}
|
|
|
|
/* Show fullscreen views above top-layer */
|
|
if (view->output) {
|
|
desktop_update_top_layer_visibility(view->server);
|
|
}
|
|
}
|
|
|
|
void
|
|
view_set_fullscreen(struct view *view, bool fullscreen)
|
|
{
|
|
assert(view);
|
|
if (fullscreen == view->fullscreen) {
|
|
return;
|
|
}
|
|
if (fullscreen) {
|
|
if (!output_is_usable(view->output)) {
|
|
/* Prevent fullscreen with no available outputs */
|
|
return;
|
|
}
|
|
/*
|
|
* Fullscreen via keybind or client request cancels
|
|
* interactive move/resize since we can't move/resize
|
|
* a fullscreen view.
|
|
*/
|
|
interactive_cancel(view);
|
|
view_store_natural_geometry(view);
|
|
view_invalidate_last_layout_geometry(view);
|
|
}
|
|
|
|
set_fullscreen(view, fullscreen);
|
|
if (view_is_floating(view)) {
|
|
view_apply_natural_geometry(view);
|
|
} else {
|
|
view_apply_special_geometry(view);
|
|
}
|
|
set_adaptive_sync_fullscreen(view);
|
|
}
|
|
|
|
static bool
|
|
last_layout_geometry_is_valid(struct view *view)
|
|
{
|
|
return view->last_layout_geometry.width > 0
|
|
&& view->last_layout_geometry.height > 0;
|
|
}
|
|
|
|
static void
|
|
update_last_layout_geometry(struct view *view)
|
|
{
|
|
/*
|
|
* Only update an invalid last-layout geometry to prevent a series of
|
|
* successive layout changes from continually replacing the "preferred"
|
|
* location with whatever location the view currently holds. The
|
|
* "preferred" location should be whatever state was set by user
|
|
* interaction, not automatic responses to layout changes.
|
|
*/
|
|
if (last_layout_geometry_is_valid(view)) {
|
|
return;
|
|
}
|
|
|
|
if (view_is_floating(view)) {
|
|
view->last_layout_geometry = view->pending;
|
|
} else {
|
|
view->last_layout_geometry = view->natural_geometry;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
apply_last_layout_geometry(struct view *view, bool force_update)
|
|
{
|
|
/* Only apply a valid last-layout geometry */
|
|
if (!last_layout_geometry_is_valid(view)) {
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Unless forced, the last-layout geometry is only applied
|
|
* when the relevant view geometry is distinct.
|
|
*/
|
|
if (!force_update) {
|
|
struct wlr_box *relevant = view_is_floating(view) ?
|
|
&view->pending : &view->natural_geometry;
|
|
|
|
if (wlr_box_equal(relevant, &view->last_layout_geometry)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
view->natural_geometry = view->last_layout_geometry;
|
|
adjust_floating_geometry(view, &view->natural_geometry,
|
|
/* midpoint_visibility */ true);
|
|
return true;
|
|
}
|
|
|
|
void
|
|
view_invalidate_last_layout_geometry(struct view *view)
|
|
{
|
|
assert(view);
|
|
view->last_layout_geometry.width = 0;
|
|
view->last_layout_geometry.height = 0;
|
|
}
|
|
|
|
void
|
|
view_adjust_for_layout_change(struct view *view)
|
|
{
|
|
assert(view);
|
|
|
|
bool is_floating = view_is_floating(view);
|
|
bool use_natural = false;
|
|
|
|
if (!output_is_usable(view->output)) {
|
|
/* A view losing an output should have a last-layout geometry */
|
|
update_last_layout_geometry(view);
|
|
}
|
|
|
|
/* Capture a pointer to the last-layout geometry (only if valid) */
|
|
struct wlr_box *last_geometry = NULL;
|
|
if (last_layout_geometry_is_valid(view)) {
|
|
last_geometry = &view->last_layout_geometry;
|
|
}
|
|
|
|
/*
|
|
* Check if an output change is required:
|
|
* - Floating views are always mapped to the nearest output
|
|
* - Any view without a usable output needs to be repositioned
|
|
* - Any view with a valid last-layout geometry might be better
|
|
* positioned on another output
|
|
*/
|
|
if (is_floating || last_geometry || !output_is_usable(view->output)) {
|
|
/* Move the view to an appropriate output, if needed */
|
|
bool output_changed = view_discover_output(view, last_geometry);
|
|
|
|
/*
|
|
* Try to apply the last-layout to the natural geometry
|
|
* (adjusting to ensure that it fits on the screen). This is
|
|
* forced if the output has changed, but will be done
|
|
* opportunistically even on the same output if the last-layout
|
|
* geometry is different from the view's governing geometry.
|
|
*/
|
|
if (apply_last_layout_geometry(view, output_changed)) {
|
|
use_natural = true;
|
|
}
|
|
|
|
/*
|
|
* Whether or not the view has moved, the layout has changed.
|
|
* Ensure that the view now has a valid last-layout geometry.
|
|
*/
|
|
update_last_layout_geometry(view);
|
|
}
|
|
|
|
if (!is_floating) {
|
|
view_apply_special_geometry(view);
|
|
} else if (use_natural) {
|
|
/*
|
|
* Move the window to its natural location, because
|
|
* we are trying to restore a prior layout.
|
|
*/
|
|
view_apply_natural_geometry(view);
|
|
} else {
|
|
/* Otherwise, just ensure the view is on screen. */
|
|
struct wlr_box geometry = view->pending;
|
|
if (adjust_floating_geometry(view, &geometry,
|
|
/* midpoint_visibility */ true)) {
|
|
view_move_resize(view, geometry);
|
|
}
|
|
}
|
|
|
|
view_update_outputs(view);
|
|
}
|
|
|
|
void
|
|
view_evacuate_region(struct view *view)
|
|
{
|
|
assert(view);
|
|
assert(view->tiled_region);
|
|
if (!view->tiled_region_evacuate) {
|
|
view->tiled_region_evacuate = xstrdup(view->tiled_region->name);
|
|
}
|
|
view->tiled_region = NULL;
|
|
}
|
|
|
|
void
|
|
view_on_output_destroy(struct view *view)
|
|
{
|
|
assert(view);
|
|
view->output = NULL;
|
|
}
|
|
|
|
static int
|
|
shift_view_to_usable_1d(int size,
|
|
int cur_pos, int cur_lo, int cur_extent,
|
|
int next_pos, int next_lo, int next_extent,
|
|
int margin_lo, int margin_hi)
|
|
{
|
|
int cur_min = cur_lo + rc.gap + margin_lo;
|
|
int cur_max = cur_lo + cur_extent - rc.gap - margin_hi;
|
|
|
|
int next_min = next_lo + rc.gap + margin_lo;
|
|
int next_max = next_lo + next_extent - rc.gap - margin_hi;
|
|
|
|
/*
|
|
* If the view is fully within the usable area of its original display,
|
|
* ensure that it is also fully within the usable area of the target.
|
|
*/
|
|
if (cur_pos >= cur_min && cur_pos + size <= cur_max) {
|
|
if (next_pos >= next_min && next_pos + size > next_max) {
|
|
next_pos = next_max - size;
|
|
}
|
|
|
|
return MAX(next_pos, next_min);
|
|
}
|
|
|
|
/*
|
|
* If the view was not fully within the usable area of its original
|
|
* display, kick it onscreen if its midpoint will be off the target.
|
|
*/
|
|
int midpoint = next_pos + size / 2;
|
|
if (next_pos >= next_min && midpoint > next_lo + next_extent) {
|
|
next_pos = next_max - size;
|
|
}
|
|
|
|
return MAX(next_pos, next_min);
|
|
}
|
|
|
|
void
|
|
view_move_to_edge(struct view *view, enum lab_edge direction, bool snap_to_windows)
|
|
{
|
|
assert(view);
|
|
if (!output_is_usable(view->output)) {
|
|
wlr_log(WLR_ERROR, "view has no output, not moving to edge");
|
|
return;
|
|
}
|
|
|
|
int dx = 0, dy = 0;
|
|
snap_move_to_edge(view, direction, snap_to_windows, &dx, &dy);
|
|
|
|
if (dx != 0 || dy != 0) {
|
|
/* Move the window if a change was discovered */
|
|
view_move(view, view->pending.x + dx, view->pending.y + dy);
|
|
return;
|
|
}
|
|
|
|
/* If the view is maximized, do not attempt to jump displays */
|
|
if (view->maximized != VIEW_AXIS_NONE) {
|
|
return;
|
|
}
|
|
|
|
/* Otherwise, move to edge of next adjacent display, if possible */
|
|
struct output *output =
|
|
output_get_adjacent(view->output, direction, /* wrap */ false);
|
|
if (!output) {
|
|
return;
|
|
}
|
|
|
|
/* When jumping to next output, attach to edge nearest the motion */
|
|
struct wlr_box usable = output_usable_area_in_layout_coords(output);
|
|
struct border margin = ssd_get_margin(view->ssd);
|
|
|
|
/* Bounds of the possible placement zone in this output */
|
|
int left = usable.x + rc.gap + margin.left;
|
|
int right = usable.x + usable.width - rc.gap - margin.right;
|
|
int top = usable.y + rc.gap + margin.top;
|
|
int bottom = usable.y + usable.height - rc.gap - margin.bottom;
|
|
|
|
/* Default target position on new output is current target position */
|
|
int destination_x = view->pending.x;
|
|
int destination_y = view->pending.y;
|
|
|
|
/* Compute the new position in the direction of motion */
|
|
direction = lab_edge_invert(direction);
|
|
switch (direction) {
|
|
case LAB_EDGE_LEFT:
|
|
destination_x = left;
|
|
break;
|
|
case LAB_EDGE_RIGHT:
|
|
destination_x = right - view->pending.width;
|
|
break;
|
|
case LAB_EDGE_UP:
|
|
destination_y = top;
|
|
break;
|
|
case LAB_EDGE_DOWN:
|
|
destination_y = bottom
|
|
- view_effective_height(view, /* use_pending */ true);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
struct wlr_box original_usable =
|
|
output_usable_area_in_layout_coords(view->output);
|
|
|
|
/* Make sure the window is appropriately in view along the x direction */
|
|
destination_x = shift_view_to_usable_1d(view->pending.width,
|
|
view->pending.x, original_usable.x, original_usable.width,
|
|
destination_x, usable.x, usable.width, margin.left, margin.right);
|
|
|
|
/* Make sure the window is appropriately in view along the y direction */
|
|
int eff_height = view_effective_height(view, /* use_pending */ true);
|
|
destination_y = shift_view_to_usable_1d(eff_height,
|
|
view->pending.y, original_usable.y, original_usable.height,
|
|
destination_y, usable.y, usable.height, margin.top, margin.bottom);
|
|
|
|
view_set_untiled(view);
|
|
view_set_output(view, output);
|
|
view_move(view, destination_x, destination_y);
|
|
}
|
|
|
|
void
|
|
view_grow_to_edge(struct view *view, enum lab_edge direction)
|
|
{
|
|
assert(view);
|
|
/* 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)) {
|
|
wlr_log(WLR_ERROR, "view has no output, not growing view");
|
|
return;
|
|
}
|
|
|
|
view_set_shade(view, false);
|
|
|
|
struct wlr_box geo;
|
|
snap_grow_to_next_edge(view, direction, &geo);
|
|
view_move_resize(view, geo);
|
|
}
|
|
|
|
void
|
|
view_shrink_to_edge(struct view *view, enum lab_edge direction)
|
|
{
|
|
assert(view);
|
|
|
|
/* 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)) {
|
|
wlr_log(WLR_ERROR, "view has no output, not shrinking view");
|
|
return;
|
|
}
|
|
|
|
view_set_shade(view, false);
|
|
|
|
struct wlr_box geo = view->pending;
|
|
snap_shrink_to_next_edge(view, direction, &geo);
|
|
view_move_resize(view, geo);
|
|
}
|
|
|
|
enum view_axis
|
|
view_axis_parse(const char *direction)
|
|
{
|
|
if (!direction) {
|
|
return VIEW_AXIS_INVALID;
|
|
}
|
|
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 if (!strcasecmp(direction, "none")) {
|
|
return VIEW_AXIS_NONE;
|
|
} else {
|
|
return VIEW_AXIS_INVALID;
|
|
}
|
|
}
|
|
|
|
enum lab_placement_policy
|
|
view_placement_parse(const char *policy)
|
|
{
|
|
if (!policy) {
|
|
return LAB_PLACE_CENTER;
|
|
}
|
|
|
|
if (!strcasecmp(policy, "automatic")) {
|
|
return LAB_PLACE_AUTOMATIC;
|
|
} else if (!strcasecmp(policy, "cursor")) {
|
|
return LAB_PLACE_CURSOR;
|
|
} else if (!strcasecmp(policy, "center")) {
|
|
return LAB_PLACE_CENTER;
|
|
} else if (!strcasecmp(policy, "cascade")) {
|
|
return LAB_PLACE_CASCADE;
|
|
}
|
|
|
|
return LAB_PLACE_INVALID;
|
|
}
|
|
|
|
void
|
|
view_snap_to_edge(struct view *view, enum lab_edge edge,
|
|
bool across_outputs, bool store_natural_geometry)
|
|
{
|
|
assert(view);
|
|
|
|
if (view->fullscreen) {
|
|
return;
|
|
}
|
|
|
|
struct output *output = view->output;
|
|
if (!output_is_usable(output)) {
|
|
wlr_log(WLR_ERROR, "view has no output, not snapping to edge");
|
|
return;
|
|
}
|
|
|
|
view_set_shade(view, false);
|
|
|
|
if (across_outputs && view->tiled == edge && view->maximized == VIEW_AXIS_NONE) {
|
|
/* We are already tiled for this edge; try to switch outputs */
|
|
output = output_get_adjacent(view->output, edge, /* wrap */ false);
|
|
|
|
if (!output) {
|
|
/*
|
|
* No more output to move to
|
|
*
|
|
* We re-apply the tiled geometry without changing any
|
|
* state because the window might have been moved away
|
|
* (and thus got untiled) and then snapped back to the
|
|
* original edge.
|
|
*/
|
|
view_apply_tiled_geometry(view);
|
|
return;
|
|
}
|
|
|
|
/* When switching outputs, jump to the opposite edge */
|
|
edge = lab_edge_invert(edge);
|
|
}
|
|
|
|
if (view->maximized != VIEW_AXIS_NONE) {
|
|
/* Unmaximize + keep using existing natural_geometry */
|
|
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);
|
|
view_invalidate_last_layout_geometry(view);
|
|
}
|
|
view_set_untiled(view);
|
|
view_set_output(view, output);
|
|
view->tiled = edge;
|
|
view_notify_tiled(view);
|
|
view_apply_tiled_geometry(view);
|
|
}
|
|
|
|
void
|
|
view_snap_to_region(struct view *view, struct region *region,
|
|
bool store_natural_geometry)
|
|
{
|
|
assert(view);
|
|
assert(region);
|
|
|
|
if (view->fullscreen) {
|
|
return;
|
|
}
|
|
|
|
/* view_apply_region_geometry() needs a usable output */
|
|
if (!output_is_usable(view->output)) {
|
|
wlr_log(WLR_ERROR, "view has no output, not snapping to region");
|
|
return;
|
|
}
|
|
|
|
view_set_shade(view, false);
|
|
|
|
if (view->maximized != VIEW_AXIS_NONE) {
|
|
/* Unmaximize + keep using existing natural_geometry */
|
|
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);
|
|
view_invalidate_last_layout_geometry(view);
|
|
}
|
|
view_set_untiled(view);
|
|
view->tiled_region = region;
|
|
view_notify_tiled(view);
|
|
view_apply_region_geometry(view);
|
|
}
|
|
|
|
void
|
|
view_move_to_output(struct view *view, struct output *output)
|
|
{
|
|
assert(view);
|
|
|
|
view_invalidate_last_layout_geometry(view);
|
|
view_set_output(view, output);
|
|
if (view_is_floating(view)) {
|
|
struct wlr_box output_area = output_usable_area_in_layout_coords(output);
|
|
view->pending.x = output_area.x;
|
|
view->pending.y = output_area.y;
|
|
view_place_by_policy(view,
|
|
/* allow_cursor */ false, rc.placement_policy);
|
|
} else if (view->fullscreen) {
|
|
view_apply_fullscreen_geometry(view);
|
|
} else if (view->maximized != VIEW_AXIS_NONE) {
|
|
view_apply_maximized_geometry(view);
|
|
} else if (view->tiled) {
|
|
view_apply_tiled_geometry(view);
|
|
} else if (view->tiled_region) {
|
|
struct region *region = regions_from_name(view->tiled_region->name, output);
|
|
view_snap_to_region(view, region, /*store_natural_geometry*/ false);
|
|
}
|
|
}
|
|
|
|
static void
|
|
for_each_subview(struct view *view, void (*action)(struct view *))
|
|
{
|
|
struct wl_array subviews;
|
|
struct view **subview;
|
|
|
|
wl_array_init(&subviews);
|
|
view_append_children(view, &subviews);
|
|
wl_array_for_each(subview, &subviews) {
|
|
action(*subview);
|
|
}
|
|
wl_array_release(&subviews);
|
|
}
|
|
|
|
static void
|
|
move_to_front(struct view *view)
|
|
{
|
|
wl_list_remove(&view->link);
|
|
wl_list_insert(&view->server->views, &view->link);
|
|
wlr_scene_node_raise_to_top(&view->scene_tree->node);
|
|
}
|
|
|
|
static void
|
|
move_to_back(struct view *view)
|
|
{
|
|
wl_list_remove(&view->link);
|
|
wl_list_append(&view->server->views, &view->link);
|
|
wlr_scene_node_lower_to_bottom(&view->scene_tree->node);
|
|
}
|
|
|
|
/*
|
|
* In the view_move_to_{front,back} functions, a modal dialog is always
|
|
* shown above its parent window, and the two always move together, so
|
|
* other windows cannot come between them.
|
|
* This is consistent with GTK3/Qt5 applications on mutter and openbox.
|
|
*/
|
|
void
|
|
view_move_to_front(struct view *view)
|
|
{
|
|
assert(view);
|
|
|
|
struct view *root = view_get_root(view);
|
|
assert(root);
|
|
|
|
move_to_front(root);
|
|
for_each_subview(root, move_to_front);
|
|
/* make sure view is in front of other sub-views */
|
|
if (view != root) {
|
|
move_to_front(view);
|
|
}
|
|
|
|
cursor_update_focus(view->server);
|
|
desktop_update_top_layer_visibility(view->server);
|
|
}
|
|
|
|
void
|
|
view_move_to_back(struct view *view)
|
|
{
|
|
assert(view);
|
|
struct view *root = view_get_root(view);
|
|
assert(root);
|
|
|
|
for_each_subview(root, move_to_back);
|
|
move_to_back(root);
|
|
|
|
cursor_update_focus(view->server);
|
|
desktop_update_top_layer_visibility(view->server);
|
|
}
|
|
|
|
struct view *
|
|
view_get_root(struct view *view)
|
|
{
|
|
assert(view);
|
|
if (view->impl->get_root) {
|
|
return view->impl->get_root(view);
|
|
}
|
|
return view;
|
|
}
|
|
|
|
void
|
|
view_append_children(struct view *view, struct wl_array *children)
|
|
{
|
|
assert(view);
|
|
if (view->impl->append_children) {
|
|
view->impl->append_children(view, children);
|
|
}
|
|
}
|
|
|
|
struct view *
|
|
view_get_modal_dialog(struct view *view)
|
|
{
|
|
assert(view);
|
|
if (!view->impl->is_modal_dialog) {
|
|
return NULL;
|
|
}
|
|
/* check view itself first */
|
|
if (view->impl->is_modal_dialog(view)) {
|
|
return view;
|
|
}
|
|
|
|
/* check sibling views */
|
|
struct view *dialog = NULL;
|
|
struct view *root = view_get_root(view);
|
|
struct wl_array children;
|
|
struct view **child;
|
|
|
|
wl_array_init(&children);
|
|
view_append_children(root, &children);
|
|
wl_array_for_each(child, &children) {
|
|
if (view->impl->is_modal_dialog(*child)) {
|
|
dialog = *child;
|
|
break;
|
|
}
|
|
}
|
|
wl_array_release(&children);
|
|
return dialog;
|
|
}
|
|
|
|
bool
|
|
view_has_strut_partial(struct view *view)
|
|
{
|
|
assert(view);
|
|
return view->impl->has_strut_partial &&
|
|
view->impl->has_strut_partial(view);
|
|
}
|
|
|
|
/* Note: It is safe to assume that this function never returns NULL */
|
|
const char *
|
|
view_get_string_prop(struct view *view, const char *prop)
|
|
{
|
|
assert(view);
|
|
assert(prop);
|
|
if (view->impl->get_string_prop) {
|
|
const char *ret = view->impl->get_string_prop(view, prop);
|
|
return ret ? ret : "";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
void
|
|
view_update_title(struct view *view)
|
|
{
|
|
assert(view);
|
|
ssd_update_title(view->ssd);
|
|
wl_signal_emit_mutable(&view->events.new_title, NULL);
|
|
}
|
|
|
|
void
|
|
view_update_app_id(struct view *view)
|
|
{
|
|
assert(view);
|
|
wl_signal_emit_mutable(&view->events.new_app_id, NULL);
|
|
}
|
|
|
|
void
|
|
view_reload_ssd(struct view *view)
|
|
{
|
|
assert(view);
|
|
if (view->ssd_mode && !view->fullscreen) {
|
|
undecorate(view);
|
|
decorate(view);
|
|
}
|
|
}
|
|
|
|
int
|
|
view_get_min_width(void)
|
|
{
|
|
int button_count_left = wl_list_length(&rc.title_buttons_left);
|
|
int button_count_right = wl_list_length(&rc.title_buttons_right);
|
|
return (rc.theme->window_button_width * (button_count_left + button_count_right)) +
|
|
(rc.theme->window_button_spacing * MAX((button_count_right - 1), 0)) +
|
|
(rc.theme->window_button_spacing * MAX((button_count_left - 1), 0)) +
|
|
(2 * rc.theme->window_titlebar_padding_width);
|
|
}
|
|
|
|
void
|
|
view_toggle_keybinds(struct view *view)
|
|
{
|
|
assert(view);
|
|
view->inhibits_keybinds = !view->inhibits_keybinds;
|
|
|
|
if (view->ssd_mode) {
|
|
ssd_enable_keybind_inhibit_indicator(view->ssd,
|
|
view->inhibits_keybinds);
|
|
}
|
|
}
|
|
|
|
bool
|
|
view_inhibits_actions(struct view *view, struct wl_list *actions)
|
|
{
|
|
return view && view->inhibits_keybinds && !actions_contain_toggle_keybinds(actions);
|
|
}
|
|
|
|
void
|
|
mappable_connect(struct mappable *mappable, struct wlr_surface *surface,
|
|
wl_notify_func_t notify_map, wl_notify_func_t notify_unmap)
|
|
{
|
|
assert(mappable);
|
|
assert(!mappable->connected);
|
|
mappable->map.notify = notify_map;
|
|
wl_signal_add(&surface->events.map, &mappable->map);
|
|
mappable->unmap.notify = notify_unmap;
|
|
wl_signal_add(&surface->events.unmap, &mappable->unmap);
|
|
mappable->connected = true;
|
|
}
|
|
|
|
void
|
|
mappable_disconnect(struct mappable *mappable)
|
|
{
|
|
assert(mappable);
|
|
assert(mappable->connected);
|
|
wl_list_remove(&mappable->map.link);
|
|
wl_list_remove(&mappable->unmap.link);
|
|
mappable->connected = false;
|
|
}
|
|
|
|
static void
|
|
handle_map(struct wl_listener *listener, void *data)
|
|
{
|
|
struct view *view = wl_container_of(listener, view, mappable.map);
|
|
if (view->minimized) {
|
|
/*
|
|
* The view->impl functions do not directly support
|
|
* mapping a view while minimized. Instead, mark it as
|
|
* not minimized, map it, and then minimize it again.
|
|
*/
|
|
view->minimized = false;
|
|
view->impl->map(view);
|
|
view_minimize(view, true);
|
|
} else {
|
|
view->impl->map(view);
|
|
}
|
|
}
|
|
|
|
static void
|
|
handle_unmap(struct wl_listener *listener, void *data)
|
|
{
|
|
struct view *view = wl_container_of(listener, view, mappable.unmap);
|
|
view->impl->unmap(view, /* client_request */ true);
|
|
}
|
|
|
|
/*
|
|
* TODO: after the release of wlroots 0.17, consider incorporating this
|
|
* function into a more general view_set_surface() function, which could
|
|
* connect other surface event handlers (like commit) as well.
|
|
*/
|
|
void
|
|
view_connect_map(struct view *view, struct wlr_surface *surface)
|
|
{
|
|
assert(view);
|
|
mappable_connect(&view->mappable, surface, handle_map, handle_unmap);
|
|
}
|
|
|
|
void
|
|
view_set_shade(struct view *view, bool shaded)
|
|
{
|
|
assert(view);
|
|
|
|
if (view->shaded == shaded) {
|
|
return;
|
|
}
|
|
|
|
/* Views without a title-bar or SSD cannot be shaded */
|
|
if (shaded && (!view->ssd || !view_titlebar_visible(view))) {
|
|
return;
|
|
}
|
|
|
|
/* If this window is being resized, cancel the resize when shading */
|
|
if (shaded && view->server->input_mode == LAB_INPUT_STATE_RESIZE) {
|
|
interactive_cancel(view);
|
|
}
|
|
|
|
view->shaded = shaded;
|
|
ssd_enable_shade(view->ssd, view->shaded);
|
|
wlr_scene_node_set_enabled(&view->content_tree->node, !view->shaded);
|
|
}
|
|
|
|
void
|
|
view_set_icon(struct view *view, const char *icon_name, struct wl_array *buffers)
|
|
{
|
|
/* Update icon name */
|
|
zfree(view->icon.name);
|
|
if (icon_name) {
|
|
view->icon.name = xstrdup(icon_name);
|
|
}
|
|
|
|
/* Update icon images */
|
|
struct lab_data_buffer **buffer;
|
|
wl_array_for_each(buffer, &view->icon.buffers) {
|
|
wlr_buffer_drop(&(*buffer)->base);
|
|
}
|
|
wl_array_release(&view->icon.buffers);
|
|
wl_array_init(&view->icon.buffers);
|
|
if (buffers) {
|
|
wl_array_copy(&view->icon.buffers, buffers);
|
|
}
|
|
|
|
wl_signal_emit_mutable(&view->events.set_icon, NULL);
|
|
}
|
|
|
|
void
|
|
view_init(struct view *view)
|
|
{
|
|
assert(view);
|
|
|
|
wl_signal_init(&view->events.new_app_id);
|
|
wl_signal_init(&view->events.new_title);
|
|
wl_signal_init(&view->events.new_outputs);
|
|
wl_signal_init(&view->events.maximized);
|
|
wl_signal_init(&view->events.minimized);
|
|
wl_signal_init(&view->events.fullscreened);
|
|
wl_signal_init(&view->events.activated);
|
|
wl_signal_init(&view->events.set_icon);
|
|
wl_signal_init(&view->events.destroy);
|
|
}
|
|
|
|
void
|
|
view_destroy(struct view *view)
|
|
{
|
|
assert(view);
|
|
struct server *server = view->server;
|
|
|
|
wl_signal_emit_mutable(&view->events.destroy, NULL);
|
|
snap_constraints_invalidate(view);
|
|
|
|
if (view->mappable.connected) {
|
|
mappable_disconnect(&view->mappable);
|
|
}
|
|
|
|
wl_list_remove(&view->request_move.link);
|
|
wl_list_remove(&view->request_resize.link);
|
|
wl_list_remove(&view->request_minimize.link);
|
|
wl_list_remove(&view->request_maximize.link);
|
|
wl_list_remove(&view->request_fullscreen.link);
|
|
wl_list_remove(&view->set_title.link);
|
|
wl_list_remove(&view->destroy.link);
|
|
|
|
if (view->foreign_toplevel) {
|
|
foreign_toplevel_destroy(view->foreign_toplevel);
|
|
view->foreign_toplevel = NULL;
|
|
}
|
|
|
|
if (server->grabbed_view == view) {
|
|
/* Application got killed while moving around */
|
|
interactive_cancel(view);
|
|
}
|
|
|
|
if (server->active_view == view) {
|
|
server->active_view = NULL;
|
|
}
|
|
|
|
if (server->session_lock_manager->last_active_view == view) {
|
|
server->session_lock_manager->last_active_view = NULL;
|
|
}
|
|
|
|
if (server->seat.pressed.view == view) {
|
|
seat_reset_pressed(&server->seat);
|
|
}
|
|
|
|
if (view->tiled_region_evacuate) {
|
|
zfree(view->tiled_region_evacuate);
|
|
}
|
|
|
|
osd_on_view_destroy(view);
|
|
undecorate(view);
|
|
|
|
view_set_icon(view, NULL, NULL);
|
|
|
|
/*
|
|
* The layer-shell top-layer is disabled when an application is running
|
|
* in fullscreen mode, so if that's the case, we may have to re-enable
|
|
* it here.
|
|
*/
|
|
if (view->fullscreen && view->output) {
|
|
view->fullscreen = false;
|
|
desktop_update_top_layer_visibility(server);
|
|
if (rc.adaptive_sync == LAB_ADAPTIVE_SYNC_FULLSCREEN) {
|
|
set_adaptive_sync_fullscreen(view);
|
|
}
|
|
}
|
|
|
|
menu_on_view_destroy(view);
|
|
|
|
/*
|
|
* Destroy the view's scene tree. View methods assume this is non-NULL,
|
|
* so we should avoid any calls to those between this and freeing the
|
|
* view.
|
|
*/
|
|
if (view->scene_tree) {
|
|
wlr_scene_node_destroy(&view->scene_tree->node);
|
|
view->scene_tree = NULL;
|
|
}
|
|
|
|
assert(wl_list_empty(&view->events.new_app_id.listener_list));
|
|
assert(wl_list_empty(&view->events.new_title.listener_list));
|
|
assert(wl_list_empty(&view->events.new_outputs.listener_list));
|
|
assert(wl_list_empty(&view->events.maximized.listener_list));
|
|
assert(wl_list_empty(&view->events.minimized.listener_list));
|
|
assert(wl_list_empty(&view->events.fullscreened.listener_list));
|
|
assert(wl_list_empty(&view->events.activated.listener_list));
|
|
assert(wl_list_empty(&view->events.set_icon.listener_list));
|
|
assert(wl_list_empty(&view->events.destroy.listener_list));
|
|
|
|
/* Remove view from server->views */
|
|
wl_list_remove(&view->link);
|
|
free(view);
|
|
|
|
cursor_update_focus(server);
|
|
}
|