genode/repos/gems/src/app/window_layouter/window.h
Norman Feske 7e7eff0eb7 window layouter: reduce snap-back artifact
When resizing windows of clients that respond very slowly to resize
requests, the window's size sometimes snapped back to its original size
immediately after finishing the drag operation.

The problem was caused by the interplay of the layout rules (obtained
via the 'rules' ROM, generated by the 'rules' report) and the
temporary interactive state that occurs during drag operations.
The rules are updated only at the time of releasing the button to keep
the overhead while dragging the window low. However, when releasing the
mouse, the (now outdated) rules kicked back into effect, triggering
resize requests for the window to its old size.

The patch solves this problem by decoupling the dragged state of a
window from the physical release of the button. The button release
triggers a transition from a DRAGGING to a SETTLING state and programs
a timer. In the SETTLING state, the windows behave as in DRAGGING state,
giving the interactive geometry precedence over the rules-dictated
geometry. During this state, further responses of window-resize requests
may come in and are handled like dragging was still in progress. After a
timeout, however, the current window layout is conserved as a new rules
report and the state goes back to IDLE.

For clients that takes a very long time (in particular, VirtualBox when
resizing the desktop, which takes sometimes multiple seconds), the
snap-back artifact can still occur, but the effect is reduced.
2019-07-09 08:58:38 +02:00

508 lines
13 KiB
C++

/*
* \brief Window layouter
* \author Norman Feske
* \date 2013-02-14
*/
/*
* Copyright (C) 2013-2017 Genode Labs GmbH
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _WINDOW_H_
#define _WINDOW_H_
/* Genode includes */
#include <util/list_model.h>
/* local includes */
#include <types.h>
#include <focus_history.h>
#include <assign.h>
#include <decorator_margins.h>
namespace Window_layouter { class Window; }
class Window_layouter::Window : public List_model<Window>::Element
{
public:
typedef String<256> Title;
typedef String<256> Label;
struct Element
{
enum Type { UNDEFINED, TITLE, LEFT, RIGHT, TOP, BOTTOM,
TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT,
CLOSER, MAXIMIZER, MINIMIZER };
Type type;
char const *name() const
{
switch (type) {
case UNDEFINED: return "";
case TITLE: return "title";
case LEFT: return "left";
case RIGHT: return "right";
case TOP: return "top";
case BOTTOM: return "bottom";
case TOP_LEFT: return "top_left";
case TOP_RIGHT: return "top_right";
case BOTTOM_LEFT: return "bottom_left";
case BOTTOM_RIGHT: return "bottom_right";
case CLOSER: return "closer";
case MAXIMIZER: return "maximizer";
case MINIMIZER: return "minimizer";
}
return "";
}
Element(Type type) : type(type) { }
bool operator != (Element const &other) const { return other.type != type; }
bool operator == (Element const &other) const { return other.type == type; }
bool resize_handle() const
{
return type == LEFT || type == RIGHT
|| type == TOP || type == BOTTOM
|| type == TOP_LEFT || type == TOP_RIGHT
|| type == BOTTOM_LEFT || type == BOTTOM_RIGHT
|| type == MAXIMIZER;
}
};
private:
Window_id const _id;
Title _title { };
Label const _label;
Decorator_margins const &_decorator_margins;
Rect _geometry { };
/**
* Window geometry at the start of the current drag operation
*/
Rect _orig_geometry { };
/**
* Destined window geometry as defined by the user's drag operation
*/
Rect _drag_geometry { };
Area _client_size;
/**
* Size as desired by the user during resize drag operations
*/
Area _dragged_size;
/**
* Target geometry the window is assigned to, used while maximized
*/
Rect _target_geometry { };
/**
* Desired size to be requested to the client
*/
Area _requested_size() const
{
return (_maximized || !_floating)
? _decorator_margins.inner_geometry(_target_geometry).area()
: _dragged_size;
}
/**
* Most recent resize request propagated to the window manager
*
* Initially, no resize request must be generated because the
* '_dragged_size' corresponds to the window size.
*/
Area _reported_resize_request = _dragged_size;
/**
* Window may be partially transparent
*/
bool _has_alpha = false;
/**
* Window is temporarily not visible
*/
bool _hidden = false;
bool _resizeable = false;
/**
* Toggled interactively
*/
bool _maximized = false;
/**
* Set when position is defined in the window's assign rule
*/
bool _floating = false;
bool _use_target_area() const { return _maximized || !_floating; }
bool _dragged = false;
Element _dragged_element { Element::UNDEFINED };
bool _focused = false;
Element _hovered { Element::UNDEFINED };
/*
* Value that keeps track when the window has been moved to front
* most recently. It is used as a criterion for the order of the
* generated <assign> rules.
*
* This value is also used by the decorator to detect the need for
* bringing the window to the front of nitpicker's global view stack
* even if the stacking order stays the same within the decorator
* instance. This is important in the presence of more than a single
* decorator.
*/
unsigned _to_front_cnt = 0;
Focus_history::Entry _focus_history_entry;
bool _drag_left_border = false;
bool _drag_right_border = false;
bool _drag_top_border = false;
bool _drag_bottom_border = false;
/**
* Called when the user starts dragging a window element
*/
void _initiate_drag_operation(Window::Element element)
{
_dragged_element = element;
if (_resizeable) {
_drag_left_border = (element.type == Window::Element::LEFT)
|| (element.type == Window::Element::TOP_LEFT)
|| (element.type == Window::Element::BOTTOM_LEFT);
_drag_right_border = (element.type == Window::Element::RIGHT)
|| (element.type == Window::Element::TOP_RIGHT)
|| (element.type == Window::Element::BOTTOM_RIGHT);
_drag_top_border = (element.type == Window::Element::TOP)
|| (element.type == Window::Element::TOP_LEFT)
|| (element.type == Window::Element::TOP_RIGHT);
_drag_bottom_border = (element.type == Window::Element::BOTTOM)
|| (element.type == Window::Element::BOTTOM_LEFT)
|| (element.type == Window::Element::BOTTOM_RIGHT);
}
_orig_geometry = _geometry;
_drag_geometry = _geometry;
_dragged_size = _geometry.area();
_dragged = true;
}
/**
* Called each time the pointer moves while the window is dragged
*/
void _apply_drag_operation(Point offset)
{
/* move window */
if (!_drag_border()) {
_drag_geometry = Rect(_orig_geometry.p1() + offset,
_orig_geometry.area());
return;
}
/* resize window */
int x1 = _orig_geometry.x1(), y1 = _orig_geometry.y1(),
x2 = _orig_geometry.x2(), y2 = _orig_geometry.y2();
if (_drag_left_border) x1 = min(x1 + offset.x(), x2);
if (_drag_right_border) x2 = max(x2 + offset.x(), x1);
if (_drag_top_border) y1 = min(y1 + offset.y(), y2);
if (_drag_bottom_border) y2 = max(y2 + offset.y(), y1);
_drag_geometry = Rect(Point(x1, y1), Point(x2, y2));
_dragged_size = _drag_geometry.area();
}
/**
* Return true if user drags a window border
*/
bool _drag_border() const
{
return _drag_left_border || _drag_right_border
|| _drag_top_border || _drag_bottom_border;
}
Constructible<Assign::Member> _assign_member { };
public:
Window(Window_id id, Label const &label, Area initial_size,
Focus_history &focus_history,
Decorator_margins const &decorator_margins)
:
_id(id), _label(label),
_decorator_margins(decorator_margins),
_client_size(initial_size),
_dragged_size(initial_size),
_focus_history_entry(focus_history, _id)
{ }
bool has_id(Window_id id) const { return id == _id; }
Window_id id() const { return _id; }
void title(Title const &title) { _title = title; }
Label label() const { return _label; }
bool dragged() const { return _dragged; }
Rect effective_inner_geometry() const
{
if (!_dragged)
return _geometry;
int x1 = _orig_geometry.x1(), y1 = _orig_geometry.y1(),
x2 = _orig_geometry.x2(), y2 = _orig_geometry.y2();
/* move window */
if (!_drag_border())
return _drag_geometry;
/* resize window */
if (_drag_left_border) x1 = x2 - _client_size.w() + 1;
if (_drag_right_border) x2 = x1 + _client_size.w() - 1;
if (_drag_top_border) y1 = y2 - _client_size.h() + 1;
if (_drag_bottom_border) y2 = y1 + _client_size.h() - 1;
return Rect(Point(x1, y1), Point(x2, y2));
}
/**
* Place window
*
* \param geometry outer geometry available to the window
*/
void outer_geometry(Rect outer)
{
/* drop attempts to apply layout while dragging the window */
if (_dragged)
return;
_geometry = _decorator_margins.inner_geometry(outer);
_dragged_size = _geometry.area();
}
Rect outer_geometry() const
{
return _decorator_margins.outer_geometry(_geometry);
}
Area client_size() const { return _client_size; }
void focused(bool focused) { _focused = focused; }
void hovered(Element hovered) { _hovered = hovered; }
Point position() const { return _geometry.p1(); }
void has_alpha(bool has_alpha) { _has_alpha = has_alpha; }
void hidden(bool hidden) { _hidden = hidden; }
void resizeable(bool resizeable) { _resizeable = resizeable; }
bool label_matches(Label const &label) const { return label == _label; }
/**
* Define window size
*
* This function is called when the window-list model changes.
*/
void client_size(Area size) { _client_size = size; }
/**
* Return true if a request request to the window manager is due
*/
bool resize_request_needed() const
{
/* a resize request for the current size is already in flight */
if (_requested_size() == _reported_resize_request)
return false;
return (_requested_size() != _client_size);
}
/**
* Mark the currently requested size as processed so that no further
* resize requests for the same size are generated
*/
void resize_request_updated() { _reported_resize_request = _requested_size(); }
void gen_resize_request(Xml_generator &xml) const
{
Area const size = _requested_size();
if (size == _client_size)
return;
xml.node("window", [&] () {
xml.attribute("id", _id.value);
xml.attribute("width", size.w());
xml.attribute("height", size.h());
});
}
void generate(Xml_generator &xml) const
{
/* omit window from the layout if hidden */
if (_hidden)
return;
xml.node("window", [&]() {
xml.attribute("id", _id.value);
/* present concatenation of label and title in the window's title bar */
{
bool const has_title = strlen(_title.string()) > 0;
String<Label::capacity()> const
title(_label, (has_title ? " " : ""), _title);
xml.attribute("title", title);
}
Rect const rect = _use_target_area()
? _decorator_margins.inner_geometry(_target_geometry)
: effective_inner_geometry();
xml.attribute("xpos", rect.x1());
xml.attribute("ypos", rect.y1());
/*
* Constrain size of non-floating windows
*
* In contrast to a tiled or maximized window that is hard
* constrained by the size of the target geometry even if the
* client requests a larger size, floating windows respect the
* size defined by the client.
*
* This way, while floating, applications are able to resize
* their window according to the application's state. For
* example, a bottom-right resize corner of a Qt application is
* handled by the application, not the window manager, but it
* should still work as expected by the user. Note that the
* ability of the application to influence the layout is
* restricted to its window size. The window position cannot be
* influenced. This limits the ability of an application to
* impersonate other applications.
*/
Area const size = _use_target_area()
? Area(min(rect.w(), _client_size.w()),
min(rect.h(), _client_size.h()))
: _client_size;
xml.attribute("width", size.w());
xml.attribute("height", size.h());
if (_focused)
xml.attribute("focused", "yes");
if (_dragged) {
xml.node("highlight", [&] () {
xml.node(_dragged_element.name(), [&] () {
xml.attribute("pressed", "yes"); }); });
} else {
bool const passive = (!_resizeable && _hovered.resize_handle())
|| (_hovered.type == Element::UNDEFINED);
if (!passive)
xml.node("highlight", [&] () {
xml.node(_hovered.name()); });
}
if (_has_alpha)
xml.attribute("has_alpha", "yes");
if (_resizeable) {
xml.attribute("maximizer", "yes");
xml.attribute("closer", "yes");
}
});
}
void drag(Window::Element element, Point clicked, Point curr)
{
/* prevent maximized windows from being dragged */
if (maximized())
return;
if (!_dragged)
_initiate_drag_operation(element);
_apply_drag_operation(curr - clicked);
}
void finalize_drag_operation()
{
_drag_left_border = false;
_drag_right_border = false;
_drag_top_border = false;
_drag_bottom_border = false;
_geometry = effective_inner_geometry();
_dragged_size = _geometry.area();
_dragged = false;
}
void to_front_cnt(unsigned to_front_cnt) { _to_front_cnt = to_front_cnt; }
unsigned to_front_cnt() const { return _to_front_cnt; }
void close() { _dragged_size = Area(0, 0); }
void target_geometry(Rect rect) { _target_geometry = rect; }
bool maximized() const { return _maximized; }
void maximized(bool maximized) { _maximized = maximized; }
void floating(bool floating) { _floating = floating; }
void dissolve_from_assignment() { _assign_member.destruct(); }
/**
* Associate window with a <assign> definition
*/
void assignment(Registry<Assign::Member> &registry)
{
/* retain first matching assignment only */
if (_assign_member.constructed())
return;
_assign_member.construct(registry, *this);
}
};
#endif /* _WINDOW_H_ */