Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions book/src/configuration/misc.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,32 @@ instead.
split-reuses-container = false # default
```

## Flatten Tree

When a window is removed from a container (e.g. moved to another workspace or
floated), or when pressing split-vertical/split-horizontal on a single window,
the container may end up with only one child. By default the container is kept,
resulting in a redundant nesting.

The `flatten-tree` setting controls when such containers are automatically
collapsed:

`"never"`
: Never collapse single-child containers (default).

`"always"`
: Collapse a single-child container both when its second-to-last child is
removed and when pressing split-vertical or split-horizontal on a sole
child.

`"on-remove"`
: Only collapse when the second-to-last child is removed (e.g. moved to
another workspace or floated).

```toml
flatten-tree = "on-remove"
```

## Middle-Click Paste

Controls whether middle-clicking pastes the primary selection. Changing this
Expand Down
5 changes: 5 additions & 0 deletions book/src/control-center.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,11 @@ Split Reuses Container
direction instead of wrapping the window in a new container (see
[Split Reuses Container](configuration/misc.md#split-reuses-container))

Flatten Tree
: Dropdown to select when single-child containers are flattened (`Never`,
`Always`, or `On Remove`; see
[Flatten Tree](configuration/misc.md#flatten-tree))

Font
: Text field for the main compositor font family

Expand Down
5 changes: 5 additions & 0 deletions jay-config/src/_private/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use crate::_private::logging;
use crate::Axis;
use crate::ContainerTarget;
use crate::Direction;
use crate::FlattenTree;
use crate::ModifiedKeySym;
use crate::PciId;
use crate::RelativeAxis;
Expand Down Expand Up @@ -1288,6 +1289,10 @@ impl ConfigClient {
self.send(&ClientMessage::SetMiddleClickPasteEnabled { enabled });
}

pub fn set_flatten_tree(&self, mode: FlattenTree) {
self.send(&ClientMessage::SetFlattenTree { mode });
}

pub fn open_control_center(&self) {
self.send(&ClientMessage::OpenControlCenter);
}
Expand Down
4 changes: 4 additions & 0 deletions jay-config/src/_private/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::_private::WorkspaceShowOpV2;
use crate::Axis;
use crate::ContainerTarget;
use crate::Direction;
use crate::FlattenTree;
use crate::PciId;
use crate::RelativeAxis;
use crate::Workspace;
Expand Down Expand Up @@ -1084,6 +1085,9 @@ pub enum ClientMessage<'a> {
target: ContainerTarget,
axis: RelativeAxis,
},
SetFlattenTree {
mode: FlattenTree,
},
}

#[derive(Serialize, Deserialize, Debug)]
Expand Down
21 changes: 21 additions & 0 deletions jay-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ pub enum Direction {
Right,
}

/// Configures how single-child containers are flattened.
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
#[non_exhaustive]
pub enum FlattenTree {
/// Never flatten single-child containers.
Never,
/// Always flatten single-child containers, including when pressing
/// split-vertical or split-horizontal on a sole child.
Always,
/// Only flatten when the second-to-last child is removed from a
/// container.
OnRemove,
}

/// A planar axis.
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub enum Axis {
Expand Down Expand Up @@ -597,6 +611,13 @@ pub fn set_middle_click_paste_enabled(enabled: bool) {
get!().set_middle_click_paste_enabled(enabled);
}

/// Sets the tree flattening mode.
///
/// The default is [`FlattenTree::Never`].
pub fn set_flatten_tree(mode: FlattenTree) {
get!().set_flatten_tree(mode);
}

/// Opens the control center.
pub fn open_control_center() {
get!().open_control_center();
Expand Down
1 change: 1 addition & 0 deletions src/compositor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ fn start_compositor2(
color_management_enabled: Cell::new(false),
color_manager,
float_above_fullscreen: Cell::new(false),
flatten_tree: Default::default(),
icons: Default::default(),
show_pin_icon: Cell::new(false),
cl_matcher_manager: Rc::new(ClMatcherManager::new(&crit_ids)),
Expand Down
14 changes: 14 additions & 0 deletions src/config/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ use jay_config::_private::serialize_server_message;
use jay_config::Axis;
use jay_config::ContainerTarget as ConfigContainerTarget;
use jay_config::Direction;
use jay_config::FlattenTree as ConfigFlattenTree;
use jay_config::RelativeAxis as ConfigRelativeAxis;
use jay_config::Workspace;
use jay_config::WorkspaceKind;
Expand Down Expand Up @@ -2943,6 +2944,14 @@ impl ConfigProxyHandler {
self.state.set_primary_selection_enabled(enabled);
}

fn handle_set_flatten_tree(&self, mode: ConfigFlattenTree) -> Result<(), CphError> {
let Ok(mode) = mode.try_into() else {
return Err(CphError::UnknownFlattenTree(mode));
};
self.state.set_flatten_tree(mode);
Ok(())
}

fn handle_seat_create_mark(&self, seat: Seat, kc: Option<u32>) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
if let Some(kc) = kc {
Expand Down Expand Up @@ -4056,6 +4065,9 @@ impl ConfigProxyHandler {
ClientMessage::SetMiddleClickPasteEnabled { enabled } => {
self.handle_set_middle_click_paste_enabled(enabled)
}
ClientMessage::SetFlattenTree { mode } => {
self.handle_set_flatten_tree(mode).wrn("set_flatten_tree")?
}
ClientMessage::SetWorkspaceDisplayOrder { order } => {
self.handle_set_workspace_display_order(order)
}
Expand Down Expand Up @@ -4485,6 +4497,8 @@ enum CphError {
UnknownContainerTarget(ConfigContainerTarget),
#[error("Tried to use an unknown relative axis: {0:?}")]
UnknownRelativeAxis(ConfigRelativeAxis),
#[error("Unknown flatten tree mode {0:?}")]
UnknownFlattenTree(ConfigFlattenTree),
}

trait WithRequestName {
Expand Down
19 changes: 19 additions & 0 deletions src/control_center/cc_look_and_feel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::control_center::ControlCenterInner;
use crate::control_center::bool;
use crate::control_center::bool_ui;
use crate::control_center::combo_box;
use crate::control_center::combo_box_ui;
use crate::control_center::drag_value;
use crate::control_center::grid;
use crate::control_center::grid_label_ui;
Expand Down Expand Up @@ -134,6 +135,24 @@ impl LookAndFeelPane {
self.state.split_reuses_container.get(),
|v| self.state.set_split_reuses_container(v),
);
combo_box_ui(
ui,
"Flatten Tree",
|ui| {
tip(ui, |ui| {
ui.label(concat!(
"Containers that are left with only one child are replaced by ",
"that child.",
));
ui.label(concat!(
"`Always` additionally applies this when splitting the only ",
"window in a container.",
));
});
},
self.state.flatten_tree.get(),
|v| self.state.set_flatten_tree(v),
);
row(ui, "Font", |ui| {
let mut v = self.state.theme.font.get().to_string();
if text_edit(ui, &mut v).changed() {
Expand Down
7 changes: 7 additions & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ use crate::tree::ContainerSplit;
use crate::tree::Direction;
use crate::tree::DisplayNode;
use crate::tree::FindTreeUsecase;
use crate::tree::FlattenTree;
use crate::tree::FloatNode;
use crate::tree::FoundNode;
use crate::tree::LatchListener;
Expand Down Expand Up @@ -394,6 +395,7 @@ pub struct State {
pub color_management_enabled: Cell<bool>,
pub color_manager: Rc<ColorManager>,
pub float_above_fullscreen: Cell<bool>,
pub flatten_tree: Cell<FlattenTree>,
pub icons: Icons,
pub show_pin_icon: Cell<bool>,
pub cl_matcher_manager: Rc<ClMatcherManager>,
Expand Down Expand Up @@ -2314,6 +2316,11 @@ impl State {
self.root.update_visible(self);
}

pub fn set_flatten_tree(&self, v: FlattenTree) {
self.flatten_tree.set(v);
self.trigger_cci(CCI_LOOK_AND_FEEL);
}

pub fn reset_sizes(self: &Rc<Self>) {
self.theme.sizes.reset(LiveTL);
self.spaces_changed();
Expand Down
43 changes: 43 additions & 0 deletions src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub use containing::*;
pub use display::*;
pub use float::*;
use jay_config::Direction as JayDirection;
use jay_config::FlattenTree as ConfigFlattenTree;
use jay_config::video::Transform as ConfigTransform;
use jay_config::window::TileState as ConfigTileState;
use jay_config::workspace::WorkspaceDisplayOrder as ConfigWorkspaceDisplayOrder;
Expand Down Expand Up @@ -275,6 +276,48 @@ impl From<JayDirection> for Direction {
}
}

#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Default, Linearize)]
pub enum FlattenTree {
#[default]
Never,
Always,
OnRemove,
}

impl TryFrom<ConfigFlattenTree> for FlattenTree {
type Error = ();

fn try_from(value: ConfigFlattenTree) -> Result<Self, Self::Error> {
let v = match value {
ConfigFlattenTree::Never => FlattenTree::Never,
ConfigFlattenTree::Always => FlattenTree::Always,
ConfigFlattenTree::OnRemove => FlattenTree::OnRemove,
_ => return Err(()),
};
Ok(v)
}
}

impl Into<ConfigFlattenTree> for FlattenTree {
fn into(self) -> ConfigFlattenTree {
match self {
FlattenTree::Never => ConfigFlattenTree::Never,
FlattenTree::Always => ConfigFlattenTree::Always,
FlattenTree::OnRemove => ConfigFlattenTree::OnRemove,
}
}
}

impl StaticText for FlattenTree {
fn text(&self) -> &'static str {
match self {
FlattenTree::Never => "Never",
FlattenTree::Always => "Always",
FlattenTree::OnRemove => "On Remove",
}
}
}

pub struct NodeIds {
next: NumCell<u32>,
}
Expand Down
61 changes: 48 additions & 13 deletions src/tree/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use crate::tree::ContainingNode;
use crate::tree::Direction;
use crate::tree::FindTreeResult;
use crate::tree::FindTreeUsecase;
use crate::tree::FlattenTree;
use crate::tree::FloatNode;
use crate::tree::FoundNode;
use crate::tree::Node;
Expand Down Expand Up @@ -176,6 +177,17 @@ struct MainAxisRange {
active: bool,
}

/// The outcome of an attempt to replace a container by its sole remaining child.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum ReplaceWithSoleChild {
/// The container was replaced by the child and destroyed.
Flattened,
/// The parent does not accept the child.
Rejected,
/// The container has no parent, or is fullscreen.
NotApplicable,
}

#[derive(Default)]
pub struct ContainerNodeState {
pub split: Cell<ContainerSplit>,
Expand Down Expand Up @@ -371,6 +383,28 @@ impl ContainerNode {
ref_
}

fn try_replace_with_sole_child(
self: &Rc<Self>,
child: Rc<dyn ToplevelNode>,
) -> ReplaceWithSoleChild {
let Some(parent) = self.toplevel_data.parent.get() else {
return ReplaceWithSoleChild::NotApplicable;
};
if self.toplevel_data.is_fullscreen[LiveTL].get() {
return ReplaceWithSoleChild::NotApplicable;
}
if !parent.cnode_accepts_child(&*child) {
return ReplaceWithSoleChild::Rejected;
}
parent.cnode_replace_child(self.deref(), child);
self.toplevel_data.parent.take();
for cn in self.child_nodes.borrow_mut().drain_values() {
self.schedule_unlink_child(cn);
}
self.tl_destroy();
ReplaceWithSoleChild::Flattened
}

pub fn prepend_child(self: &Rc<Self>, new: Rc<dyn ToplevelNode>) {
if let Some(child) = self.children.first() {
self.add_child_before_(&child, new);
Expand Down Expand Up @@ -1365,19 +1399,9 @@ impl ContainerNode {

// CASE 1: This is the only child of the container. Replace the container by the child.
if ns.num_children.get() == 1 {
if let Some(parent) = self.toplevel_data.parent.get()
&& !self.toplevel_data.is_fullscreen[LiveTL].get()
{
if parent.cnode_accepts_child(&*child) {
parent.cnode_replace_child(self.deref(), child.clone());
self.toplevel_data.parent.take();
for cn in self.child_nodes.borrow_mut().drain_values() {
self.schedule_unlink_child(cn);
}
self.tl_destroy();
} else {
move_to_neighboring_output(child);
}
match self.try_replace_with_sole_child(child.clone()) {
ReplaceWithSoleChild::Flattened | ReplaceWithSoleChild::NotApplicable => {}
ReplaceWithSoleChild::Rejected => move_to_neighboring_output(child),
}
return;
}
Expand Down Expand Up @@ -2400,6 +2424,17 @@ impl ContainingNode for ContainerNode {
}
let node = self.schedule_unlink_child(node);
let num_children = self.adj_ns_num_children(|value| value - 1);
if num_children == 1
&& matches!(
self.state.flatten_tree.get(),
FlattenTree::Always | FlattenTree::OnRemove
)
&& let Some(remaining) = self.children.first_valid(LiveTL)
&& self.try_replace_with_sole_child(remaining.node.clone())
== ReplaceWithSoleChild::Flattened
{
return;
}
if num_children == 0 {
self.tl_destroy();
return;
Expand Down
3 changes: 2 additions & 1 deletion src/tree/toplevel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use crate::tree::ContainerSplit;
use crate::tree::ContainerTarget;
use crate::tree::ContainingNode;
use crate::tree::Direction;
use crate::tree::FlattenTree;
use crate::tree::FloatNode;
use crate::tree::Node;
use crate::tree::NodeBase;
Expand Down Expand Up @@ -1222,7 +1223,7 @@ pub fn toplevel_create_split(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, axis:
Some(ws) => ws,
_ => return,
};
if state.split_reuses_container.get()
if (state.split_reuses_container.get() || state.flatten_tree.get() == FlattenTree::Always)
&& let Some(pn) = toplevel_parent_container(&*tl)
&& pn.node_state[LiveTL].num_children.get() == 1
{
Expand Down
Loading
Loading