window_layouter: assignment of screens to displays

This patch enhances the window layouter with the notion of displays
and the assignment of screens to displays.

Issue #5390
This commit is contained in:
Norman Feske 2024-11-12 15:57:05 +01:00 committed by Christian Helmuth
parent 21acbed65b
commit b3d99960e7
11 changed files with 445 additions and 66 deletions

View File

@ -2,15 +2,19 @@
<report rules="yes"/>
<rules>
<screen name="screen_1"/>
<screen name="screen_2"/>
<screen name="screen_3"/>
<screen name="screen_4"/>
<screen name="screen_5"/>
<screen name="screen_6"/>
<screen name="screen_7"/>
<screen name="screen_8"/>
<screen name="screen_9"/>
<display name="primary"/>
<display name="secondary"/>
<display name="ternary"/>
<screen name="screen_1" display="primary"/>
<screen name="screen_2" display="primary"/>
<screen name="screen_3" display="primary"/>
<screen name="screen_4" display="secondary"/>
<screen name="screen_5" display="secondary"/>
<screen name="screen_6" display="secondary"/>
<screen name="screen_7" display="ternary"/>
<screen name="screen_8" display="ternary"/>
<screen name="screen_9" display="ternary"/>
<screen name="screen_0"/>
<assign label_prefix="" target="screen_1" xpos="any" ypos="any"/>
</rules>

View File

@ -2,15 +2,19 @@
<report rules="yes"/>
<rules>
<screen name="screen_1"/>
<screen name="screen_2"/>
<screen name="screen_3"/>
<screen name="screen_4"/>
<screen name="screen_5"/>
<screen name="screen_6"/>
<screen name="screen_7"/>
<screen name="screen_8"/>
<screen name="screen_9"/>
<display name="primary"/>
<display name="secondary"/>
<display name="ternary"/>
<screen name="screen_1" display="primary"/>
<screen name="screen_2" display="primary"/>
<screen name="screen_3" display="primary"/>
<screen name="screen_4" display="secondary"/>
<screen name="screen_5" display="secondary"/>
<screen name="screen_6" display="secondary"/>
<screen name="screen_7" display="ternary"/>
<screen name="screen_8" display="ternary"/>
<screen name="screen_9" display="ternary"/>
<screen name="screen_0"/>
<assign label_prefix="" target="screen_1" xpos="any" ypos="any"/>
</rules>

View File

@ -101,6 +101,55 @@ or unmaximizing a window, the 'maximized' attribute of its '<assign>' rule is
toggled.
Multi-monitor support
---------------------
The layouter rules can host any number of display declarations as follows.
! <display name="primary"/>
Optional attributes 'xpos', 'ypos', 'width', and 'height' can be specified to
assign a specific rectangle of the panorama to the display. Otherwise, the
window layouter applies the following policy. The captured rectangles present
in the panorama are assigned to displays in left to right order. This gives the
opportunity to assign the notion of a "primary" or "secondary" display to
different parts of the panorama by the mere order of '<display>' nodes. If
more displays are declared than present, all unassigned displays will refer to
the left-most captured rectangle of the panorama.
To get more precise control over the assignment of captured areas to displays,
a display node can host any number of '<capture>' sub nodes that are matched
against the captured areas present within the panorama. The panorama areas are
named after the labels of capture clients (i.e., display drivers) present at
the nitpicker GUI server. The matching can be expressed via the attributes
'label', 'label_prefix', and 'label_suffix'. The first match applies.
E.g., the following configuration may be useful for a laptop that is sometimes
connected to an HDMI display at work or a Display-Port display at home.
! <display name="primary">
! <capture label_suffix="HDMI-1"/>
! <capture label_suffix="DP-2"/>
! <capture label_suffix="eDP-1"/>
! </display>
! <display name="secondary">
! <capture label_suffix="eDP-1"/>
! </display>
When neither the HDMI-1 display nor the DP-2 display is present, the laptop's
internal eDP display is used as both primary and secondary display. Once an
external display is connected, the external display acts as primary display
while the laptop's internal display takes the role of the secondary display.
Once declared, the display names can be specified as 'display' attribute to
'<screen>' nodes, thereby assigning virtual desktops to displays. Screens
referring to the same portion of the panorama are organized as a stack
where only the top-most screen is visible at a time. As each display has
its own distinct stack of screens, one screen cannot be visible at multiple
displays. To mirror the same content on multiple displays, it is best to
leverage the '<merge>' feature of the display driver. Should a '<screen>' lack
a valid display attribute, it spans the entire panorama.
Keyboard shortcuts
------------------

View File

@ -15,7 +15,6 @@
#define _ASSIGN_H_
/* Genode includes */
#include <util/list_model.h>
#include <base/registry.h>
#include <os/buffered_xml.h>
@ -108,13 +107,17 @@ class Window_layouter::Assign : public List_model<Assign>::Element
/**
* Calculate window geometry
*/
Rect window_geometry(unsigned win_id, Area client_size, Rect target_geometry,
Rect window_geometry(unsigned win_id, Area client_size, Area target_size,
Decorator_margins const &decorator_margins) const
{
if (!_pos_defined)
return target_geometry;
return { .at = { }, .area = target_size };
Point const any_pos(150*win_id % 800, 30 + (100*win_id % 500));
/* try to place new window such that it fits the target area */
unsigned const max_x = max(1, int(target_size.w) - int(client_size.w)),
max_y = max(1, int(target_size.h) - int(client_size.h));
Point const any_pos(150*win_id % max_x, 30 + (100*win_id % max_y));
Point const pos(_xpos_any ? any_pos.x : _pos.x,
_ypos_any ? any_pos.y : _pos.y);
@ -122,7 +125,7 @@ class Window_layouter::Assign : public List_model<Assign>::Element
Rect const inner(pos, _size_defined ? _size : client_size);
Rect const outer = decorator_margins.outer_geometry(inner);
return Rect(outer.p1() + target_geometry.p1(), outer.area);
return Rect(outer.p1(), outer.area);
}
bool maximized() const { return _maximized; }

View File

@ -0,0 +1,166 @@
/*
* \brief List of displays
* \author Norman Feske
* \date 2024-11-12
*/
/*
* Copyright (C) 2024 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 _DISPLAY_LIST_H_
#define _DISPLAY_LIST_H_
/* local includes */
#include <panorama.h>
namespace Window_layouter { class Display; }
struct Window_layouter::Display : List_model<Display>::Element
{
Name const name;
struct Attr
{
Rect rect; /* within panorama */
bool occupied; /* true if occupied by a screen */
} attr { };
Display(Name const &name) : name(name) { }
/**
* List_model::Element
*/
void update(Panorama const &panorama, Xml_node const &node)
{
/* import explicitly configured panorama position */
attr.rect = Rect::from_xml(node);
/* assign panorama rect according to matching display <capture> policy */
node.for_each_sub_node("capture", [&] (Xml_node const &policy) {
if (!attr.rect.valid())
panorama.with_matching_capture_rect(policy, [&] (Rect r) {
attr.rect = r; }); });
}
/**
* List_model::Element
*/
bool matches(Xml_node node) const
{
return name_from_xml(node) == name;
}
/**
* List_model::Element
*/
static bool type_matches(Xml_node const &node)
{
return node.has_type("display");
}
};
namespace Window_layouter { class Display_list; }
class Window_layouter::Display_list : Noncopyable
{
private:
Allocator &_alloc;
List_model<Display> _displays { };
Display::Attr _panorama_attr { }; /* fallback used if no display declared */
void _update_from_xml(Xml_node const &node, auto const &update_fn)
{
_displays.update_from_xml(node,
[&] (Xml_node const &node) -> Display & {
return *new (_alloc) Display(name_from_xml(node)); },
[&] (Display &display) { destroy(_alloc, &display); },
update_fn
);
}
public:
Display_list(Allocator &alloc) : _alloc(alloc) { }
~Display_list()
{
_update_from_xml(Xml_node("<empty/>"), [&] (auto &, auto &) { });
}
void update_from_xml(Panorama const &panorama, Xml_node const &node)
{
_panorama_attr.rect = panorama.rect;
/* import display definitions and their panoramic positions */
_update_from_xml(node, [&] (Display &display, Xml_node const &node) {
display.update(panorama, node); });
/* assign remaining unpositioned displays from left to right */
int min_x = 0;
_displays.for_each([&] (Display &display) {
if (!display.attr.rect.valid())
panorama.with_leftmost_captured_rect(min_x, [&] (Rect r) {
display.attr.rect = r;
min_x = r.x1() + 1; }); });
/* assign still unpositioned displays to leftmost captured rect */
_displays.for_each([&] (Display &display) {
if (!display.attr.rect.valid())
panorama.with_leftmost_captured_rect(0, [&] (Rect r) {
display.attr.rect = r; }); });
/* if nothing is captured assign the total panorama to the display */
_displays.for_each([&] (Display &display) {
if (!display.attr.rect.valid())
display.attr.rect = panorama.rect; });
}
/**
* Call 'fn' with the panorama rectangle of the display named 'name'
*/
void with_display_attr(Name const &name, auto const &fn)
{
bool done = false;
_displays.for_each([&] (Display &display) {
if (!done && display.name == name) {
fn(display.attr);
done = true; } });
if (!done)
fn(_panorama_attr);
}
void mark_as_occupied(Rect const rect)
{
_displays.for_each([&] (Display &display) {
if (rect == display.attr.rect)
display.attr.occupied = true; });
if (rect == _panorama_attr.rect)
_panorama_attr.occupied = true;
}
void reset_occupied_flags()
{
_panorama_attr.occupied = false;
_displays.for_each([&] (Display &display) {
display.attr.occupied = false; });
}
};
#endif /* _DISPLAY_LIST_H_ */

View File

@ -57,7 +57,7 @@ struct Window_layouter::Main : Operations,
Heap _heap { _env.ram(), _env.rm() };
Area _screen_size { };
Display_list _display_list { _heap };
unsigned _to_front_cnt = 1;
@ -94,9 +94,10 @@ struct Window_layouter::Main : Operations,
{
_window_list.dissolve_windows_from_assignments();
_layout_rules.with_rules([&] (Xml_node rules) {
_layout_rules.with_rules([&] (Xml_node const &rules) {
_display_list.update_from_xml(_panorama, rules);
_assign_list.update_from_xml(rules);
_target_list.update_from_xml(rules, _screen_size);
_target_list.update_from_xml(rules, _display_list);
});
_assign_list.assign_windows(_window_list);
@ -111,11 +112,11 @@ struct Window_layouter::Main : Operations,
assign.for_each_member([&] (Assign::Member &member) {
member.window.floating(assign.floating());
member.window.target_geometry(target.geometry());
member.window.target_area(target.geometry().area);
Rect const rect = assign.window_geometry(member.window.id().value,
member.window.client_size(),
target.geometry(),
target.geometry().area,
_decorator_margins);
member.window.outer_geometry(rect);
member.window.maximized(assign.maximized());
@ -154,13 +155,13 @@ struct Window_layouter::Main : Operations,
{
_config.update();
if (_config.xml().has_sub_node("report")) {
Xml_node const report = _config.xml().sub_node("report");
_rules_reporter.conditional(report.attribute_value("rules", false),
_env, "rules", "rules");
}
Xml_node const config = _config.xml();
_layout_rules.update_config(_config.xml());
config.with_optional_sub_node("report", [&] (Xml_node const &report) {
_rules_reporter.conditional(report.attribute_value("rules", false),
_env, "rules", "rules"); });
_layout_rules.update_config(config);
}
User_state _user_state { *this, _focus_history };
@ -328,12 +329,12 @@ struct Window_layouter::Main : Operations,
Gui::Connection _gui { _env };
Panorama _panorama { _heap };
void _handle_mode_change()
{
/* determine maximized window geometry */
_screen_size = _gui.panorama().convert<Gui::Area>(
[&] (Gui::Rect rect) { return rect.area; },
[&] (Gui::Undefined) { return Gui::Area { 1, 1 }; });
_gui.with_info([&] (Xml_node const &node) {
_panorama.update_from_xml(node); });
_update_window_layout();
}
@ -513,6 +514,15 @@ void Window_layouter::Main::_gen_rules_with_frontmost_screen(Target::Name const
_rules_reporter->generate([&] (Xml_generator &xml) {
_layout_rules.with_rules([&] (Xml_node const &rules) {
bool display_declared = false;
rules.for_each_sub_node("display", [&] (Xml_node const &display) {
display_declared = true;
copy_node(xml, display); });
if (display_declared)
xml.append("\n");
});
_target_list.gen_screens(xml, screen);
/*

View File

@ -0,0 +1,108 @@
/*
* \brief Internal Representation of GUI panorama
* \author Norman Feske
* \date 2024-11-12
*/
/*
* Copyright (C) 2024 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 _PANORAMA_H_
#define _PANORAMA_H_
/* local includes */
#include <types.h>
namespace Window_layouter { class Panorama; }
struct Window_layouter::Panorama
{
Allocator &_alloc;
Rect rect { };
struct Capture;
using Captures = List_model<Capture>;
Captures _captures { };
struct Capture : Captures::Element
{
Name const name { };
Rect rect { };
Capture(Name const name) : name(name) { }
/**
* List_model::Element
*/
void update(Xml_node const &node) { rect = Rect::from_xml(node); }
/**
* List_model::Element
*/
bool matches(Xml_node const &node) const
{
return name_from_xml(node) == name;
}
/**
* List_model::Element
*/
static bool type_matches(Xml_node const &node)
{
return node.has_type("capture");
}
};
Panorama(Allocator &alloc) : _alloc(alloc) { }
~Panorama() { update_from_xml("<empty/>"); }
void update_from_xml(Xml_node const &gui_info)
{
rect = Rect::from_xml(gui_info);
_captures.update_from_xml(gui_info,
[&] (Xml_node const &node) -> Capture & {
return *new (_alloc) Capture(name_from_xml(node)); },
[&] (Capture &capture) { destroy(_alloc, &capture); },
[&] (Capture &capture, Xml_node const &node) { capture.update(node); }
);
}
void with_leftmost_captured_rect(int const min_x, auto const &fn) const
{
Rect rect { };
int max_x = 1000000;
_captures.for_each([&] (Capture const &capture) {
if (capture.rect.x1() >= min_x && capture.rect.x1() < max_x) {
max_x = capture.rect.x1();
rect = capture.rect;
}
});
if (rect.valid())
fn(rect);
};
void with_matching_capture_rect(Xml_node const &policy, auto const &fn) const
{
Rect rect { };
_captures.for_each([&] (Capture const &capture) {
if (!rect.valid() && !Xml_node_label_score(policy, capture.name).conflict())
rect = capture.rect; });
if (rect.valid())
fn(rect);
}
};
#endif /* _PANORAMA_H_ */

View File

@ -26,7 +26,7 @@ class Window_layouter::Target : Noncopyable
using Name = String<64>;
enum class Visible { YES, NO };
struct Visible { bool value; };
private:
@ -42,7 +42,7 @@ class Window_layouter::Target : Noncopyable
_name (target.attribute_value("name", Name())),
_layer(target.attribute_value("layer", 9999U)),
_geometry(geometry),
_visible(visible == Visible::YES)
_visible(visible.value)
{ }
/* needed to use class as 'Registered<Target>' */

View File

@ -16,6 +16,7 @@
/* local includes */
#include <target.h>
#include <display_list.h>
namespace Window_layouter { class Target_list; }
@ -167,7 +168,7 @@ class Window_layouter::Target_list
/* found target area, iterate though all assigned windows */
assign.for_each_member([&] (Assign::Member const &member) {
member.window.generate(xml); });
member.window.generate(xml, target.geometry()); });
});
});
@ -180,32 +181,37 @@ class Window_layouter::Target_list
/*
* The 'rules' XML node is expected to contain at least one <screen>
* node. Subseqent <screen> nodes are ignored. The <screen> node may
* contain any number of <column> nodes. Each <column> node may contain
* any number of <row> nodes, which, in turn, can contain <column>
* nodes.
* node. A <screen> node may contain any number of <column> nodes. Each
* <column> node may contain any number of <row> nodes, which, in turn,
* can contain <column> nodes.
*/
void update_from_xml(Xml_node rules, Area screen_size)
void update_from_xml(Xml_node rules, Display_list &display_list)
{
_targets.for_each([&] (Registered<Target> &target) {
destroy(_alloc, &target); });
_rules.construct(_alloc, rules);
/* targets are only visible on first screen */
Target::Visible visible = Target::Visible::YES;
display_list.reset_occupied_flags();
rules.for_each_sub_node("screen", [&] (Xml_node const &screen) {
Rect const avail(Point(0, 0), screen_size);
using Attr = Display::Attr;
Name const display = screen.attribute_value("display", Name());
if (screen.attribute_value("name", Target::Name()).valid())
new (_alloc)
Registered<Target>(_targets, screen, avail, visible);
display_list.with_display_attr(display, [&] (Attr &display) {
_process_rec(screen, avail, true, visible);
/* show only one screen per display */
Target::Visible const visible { !display.occupied };
Rect const avail = display.rect;
visible = Target::Visible::NO;
if (screen.attribute_value("name", Target::Name()).valid())
new (_alloc)
Registered<Target>(_targets, screen, avail, visible);
display_list.mark_as_occupied(display.rect);
_process_rec(screen, avail, true, visible);
});
});
}

View File

@ -16,6 +16,7 @@
/* Genode includes */
#include <os/surface.h>
#include <util/list_model.h>
namespace Window_layouter {
@ -46,6 +47,37 @@ namespace Window_layouter {
};
class Window;
using Name = String<64>;
Name const name;
static Name name_from_xml(Xml_node const &node)
{
return node.attribute_value("name", Name());
}
static inline void copy_attributes(Xml_generator &xml, Xml_node const &from)
{
using Value = String<64>;
from.for_each_attribute([&] (Xml_attribute const &attr) {
Value value { };
attr.value(value);
xml.attribute(attr.name().string(), value);
});
}
struct Xml_max_depth { unsigned value; };
static inline void copy_node(Xml_generator &xml, Xml_node const &from,
Xml_max_depth max_depth = { 5 })
{
if (max_depth.value)
xml.node(from.type().string(), [&] {
copy_attributes(xml, from);
from.for_each_sub_node([&] (Xml_node const &sub_node) {
copy_node(xml, sub_node, { max_depth.value - 1 }); }); });
}
}
#endif /* _TYPES_H_ */

View File

@ -14,9 +14,6 @@
#ifndef _WINDOW_H_
#define _WINDOW_H_
/* Genode includes */
#include <util/list_model.h>
/* local includes */
#include <types.h>
#include <focus_history.h>
@ -106,9 +103,9 @@ class Window_layouter::Window : public List_model<Window>::Element
Area _dragged_size;
/**
* Target geometry the window is assigned to, used while maximized
* Target area the window can occupy, used while maximized
*/
Rect _target_geometry { };
Area _target_area { };
/**
* Desired size to be requested to the client
@ -116,7 +113,7 @@ class Window_layouter::Window : public List_model<Window>::Element
Area _requested_size() const
{
return (_maximized || !_floating)
? _decorator_margins.inner_geometry(_target_geometry).area
? _decorator_margins.inner_geometry({ { }, _target_area }).area
: _dragged_size;
}
@ -371,7 +368,7 @@ class Window_layouter::Window : public List_model<Window>::Element
});
}
void generate(Xml_generator &xml) const
void generate(Xml_generator &xml, Rect const target_rect) const
{
/* omit window from the layout if hidden */
if (_hidden)
@ -392,11 +389,11 @@ class Window_layouter::Window : public List_model<Window>::Element
}
Rect const rect = _use_target_area()
? _decorator_margins.inner_geometry(_target_geometry)
? _decorator_margins.inner_geometry({ { }, _target_area })
: effective_inner_geometry();
xml.attribute("xpos", rect.x1());
xml.attribute("ypos", rect.y1());
xml.attribute("xpos", rect.x1() + target_rect.x1());
xml.attribute("ypos", rect.y1() + target_rect.y1());
/*
* Constrain size of non-floating windows
@ -481,7 +478,7 @@ class Window_layouter::Window : public List_model<Window>::Element
void close() { _dragged_size = Area(0, 0); }
void target_geometry(Rect rect) { _target_geometry = rect; }
void target_area(Area area) { _target_area = area; };
bool maximized() const { return _maximized; }