Skip to content

Commit

Permalink
window_layouter: assignment of screens to displays
Browse files Browse the repository at this point in the history
This patch enhances the window layouter with the notion of displays
and the assignment of screens to displays.

Issue #5390
  • Loading branch information
nfeske committed Jan 30, 2025
1 parent c9863e5 commit c3c217b
Show file tree
Hide file tree
Showing 11 changed files with 444 additions and 65 deletions.
22 changes: 13 additions & 9 deletions repos/gems/recipes/raw/motif_wm/layouter.config
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
22 changes: 13 additions & 9 deletions repos/gems/recipes/raw/window_layouter/window_layouter.config
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
49 changes: 49 additions & 0 deletions repos/gems/src/app/window_layouter/README
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------------

Expand Down
13 changes: 8 additions & 5 deletions repos/gems/src/app/window_layouter/assign.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
#define _ASSIGN_H_

/* Genode includes */
#include <util/list_model.h>
#include <base/registry.h>
#include <os/buffered_xml.h>

Expand Down Expand Up @@ -108,21 +107,25 @@ 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);

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; }
Expand Down
166 changes: 166 additions & 0 deletions repos/gems/src/app/window_layouter/display_list.h
Original file line number Diff line number Diff line change
@@ -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_ */
Loading

0 comments on commit c3c217b

Please sign in to comment.