mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-15 05:12:28 +01:00
feat(scrolling): lay out the Scrolling layout with the scrolling model
The workspace keeps a ScrollingSpace of komorebi-scrolling, keyed by the id of each container, in place of recomputing equal columns from the last layout. A column can hold several windows. New commands and messages: consume, expel, toggle-column-stack, scrolling-layout-full-width, center-focus, expand and pan. Focus, move, resize, retile and closing a window go through the strip, and a drag of an edge keeps it with the rest of what the drag started from. A window that is out of view rests at the edge it left by, with 5 pixels of it on the screen and its size unchanged, and is hidden where another monitor is next to that edge. A window that will not get as narrow as its column widens the column, at the first layout after the windows have had 400 ms to draw. center_focused_column is Never, Always or OnOverflow, and still reads a boolean.
This commit is contained in:
1 parent
3f51b3a0ca
commit
db976212b1
14 files changed
+2053
-82
No files matched your search
@@ -70,7 +70,9 @@ pub use komorebi::core::MonocleFocusBehaviour;
|
||||
pub use komorebi::core::MoveBehaviour;
|
||||
pub use komorebi::core::OperationBehaviour;
|
||||
pub use komorebi::core::OperationDirection;
|
||||
pub use komorebi::core::PanDirection;
|
||||
pub use komorebi::core::Rect;
|
||||
pub use komorebi::core::ScrollingCentering;
|
||||
pub use komorebi::core::Sizing;
|
||||
pub use komorebi::core::SocketMessage;
|
||||
pub use komorebi::core::StateQuery;
|
||||
|
||||
@@ -42,9 +42,11 @@ pub use komorebi_layouts::MAX_RATIO;
|
||||
pub use komorebi_layouts::MAX_RATIOS;
|
||||
pub use komorebi_layouts::MIN_RATIO;
|
||||
pub use komorebi_layouts::OperationDirection;
|
||||
pub use komorebi_layouts::PanDirection;
|
||||
pub use komorebi_layouts::Rect;
|
||||
pub use komorebi_layouts::ScrollingLayoutOptions;
|
||||
pub use komorebi_layouts::Sizing;
|
||||
pub use komorebi_layouts::scrolling::ScrollingCentering;
|
||||
pub use komorebi_layouts::validate_ratios;
|
||||
|
||||
// Keep local modules that aren't in komorebi-layouts
|
||||
@@ -619,6 +621,13 @@ pub enum SocketMessage {
|
||||
ChangeLayout(DefaultLayout),
|
||||
CycleLayout(CycleDirection),
|
||||
ScrollingLayoutColumns(NonZeroUsize),
|
||||
ScrollingLayoutFullWidth,
|
||||
CenterFocus,
|
||||
Expand,
|
||||
Consume(OperationDirection),
|
||||
Expel,
|
||||
ToggleColumnStack,
|
||||
Pan(PanDirection),
|
||||
LayoutRatios(Option<Vec<f32>>, Option<Vec<f32>>),
|
||||
// ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
FlipLayout(Axis),
|
||||
|
||||
@@ -83,6 +83,7 @@ pub mod monitor_reconciliator;
|
||||
pub mod process_command;
|
||||
pub mod process_event;
|
||||
pub mod reconcile;
|
||||
pub mod scrolling;
|
||||
pub mod state;
|
||||
pub mod static_config;
|
||||
pub mod theme_manager;
|
||||
|
||||
@@ -69,6 +69,17 @@
|
||||
//! - **ScrollingLayoutColumns** sets how many columns the scrolling layout shows on the focused
|
||||
//! workspace, and **LayoutRatios** the share of the work area its first columns and rows take,
|
||||
//! and both tile the workspace again.
|
||||
//! - **ScrollingLayoutFullWidth** makes the focused column of the scrolling layout as wide as the
|
||||
//! screen, or puts it back, **CenterFocus** scrolls the strip so that the focused column is in
|
||||
//! the middle, once, and **Expand** makes the focused column fill the room that the columns in
|
||||
//! view leave.
|
||||
//! - **Consume** takes the first window of the column on the right, or the last of the one on the
|
||||
//! left, into the focused column, **Expel** takes the focused window out of its column into one
|
||||
//! of its own on the right, and **ToggleColumnStack** stacks the windows of the focused column
|
||||
//! or spreads the focused stack out into a column. They do nothing in the other layouts.
|
||||
//! - **Pan** moves the view of the scrolling layout a column to the left or the right, without
|
||||
//! moving the focus first, and lets it come to rest with a column against an edge of the
|
||||
//! screen ([`WindowManager::pan`]).
|
||||
//! - **InitialWorkspaceRule**, **InitialNamedWorkspaceRule**, **WorkspaceRule** and
|
||||
//! **NamedWorkspaceRule** add a rule that sends the windows of an application to a workspace,
|
||||
//! given by its indices or by its name: when they open for the first time, or always;
|
||||
@@ -235,7 +246,6 @@ use crate::core::MonocleFocusBehaviour;
|
||||
use crate::core::MoveBehaviour;
|
||||
use crate::core::OperationDirection;
|
||||
use crate::core::Rect;
|
||||
use crate::core::ScrollingLayoutOptions;
|
||||
use crate::core::SocketMessage;
|
||||
use crate::core::StateQuery;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
@@ -1750,30 +1760,36 @@ impl WindowManager {
|
||||
self.adjust_workspace_padding(sizing, adjustment)?;
|
||||
}
|
||||
SocketMessage::ScrollingLayoutColumns(count) => {
|
||||
let focused_workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let options = match focused_workspace.layout_options {
|
||||
Some(mut opts) => {
|
||||
if let Some(scrolling) = &mut opts.scrolling {
|
||||
scrolling.columns = count.into();
|
||||
}
|
||||
|
||||
opts
|
||||
}
|
||||
None => LayoutOptions {
|
||||
scrolling: Some(ScrollingLayoutOptions {
|
||||
columns: count.into(),
|
||||
center_focused_column: Default::default(),
|
||||
}),
|
||||
grid: None,
|
||||
column_ratios: None,
|
||||
row_ratios: None,
|
||||
},
|
||||
};
|
||||
|
||||
focused_workspace.layout_options = Some(options);
|
||||
self.focused_workspace_mut()?
|
||||
.set_scrolling_columns(count.into());
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::ScrollingLayoutFullWidth => {
|
||||
self.focused_workspace_mut()?.toggle_scrolling_full_width();
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::CenterFocus => {
|
||||
self.focused_workspace_mut()?.center_scrolling_column();
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::Expand => {
|
||||
self.focused_workspace_mut()?.expand_scrolling_column();
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::Consume(direction) => {
|
||||
self.focused_workspace_mut()?.consume_scrolling(direction);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::Expel => {
|
||||
self.focused_workspace_mut()?.expel_scrolling();
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::ToggleColumnStack => {
|
||||
self.focused_workspace_mut()?
|
||||
.toggle_scrolling_column_stack()?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::Pan(direction) => self.pan(direction)?,
|
||||
SocketMessage::LayoutRatios(ref columns, ref rows) => {
|
||||
use crate::core::validate_ratios;
|
||||
|
||||
|
||||
@@ -811,6 +811,7 @@ impl WindowManager {
|
||||
containers: workspace.containers().len(),
|
||||
resize_dimensions: workspace.resize_dimensions.clone(),
|
||||
latest_layout: workspace.latest_layout.clone(),
|
||||
scrolling: workspace.scrolling.clone(),
|
||||
stepped: false,
|
||||
})
|
||||
});
|
||||
@@ -842,6 +843,7 @@ impl WindowManager {
|
||||
{
|
||||
workspace.resize_dimensions = live.resize_dimensions;
|
||||
workspace.latest_layout = live.latest_layout;
|
||||
workspace.scrolling = live.scrolling;
|
||||
}
|
||||
|
||||
if let Some((wid, Some(started_from))) = pending {
|
||||
@@ -1010,6 +1012,7 @@ impl WindowManager {
|
||||
|
||||
workspace.resize_dimensions = live.resize_dimensions;
|
||||
workspace.latest_layout = live.latest_layout;
|
||||
workspace.scrolling = live.scrolling;
|
||||
}
|
||||
|
||||
if !self.apply_mouse_resize(window, started_from, new_position)? {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -226,6 +226,8 @@ impl From<&WindowManager> for State {
|
||||
.initial_window_placement_rules
|
||||
.clone(),
|
||||
promotion_swap_container_idx: None,
|
||||
scrolling: None,
|
||||
scrolling_placed: None,
|
||||
})
|
||||
.collect::<VecDeque<_>>();
|
||||
ws.focus(monitor.workspaces.focused_idx());
|
||||
|
||||
@@ -64,6 +64,12 @@ pub fn live_resize_ends(id: u32) {
|
||||
LIVE_RESIZED_WINDOWS.lock().retain(|w| *w != id);
|
||||
}
|
||||
|
||||
/// Whether the user has hold of an edge of any window, whose neighbours are on their way to
|
||||
/// where the layout puts them for as long as they do
|
||||
pub fn live_resize_in_progress() -> bool {
|
||||
!LIVE_RESIZED_WINDOWS.lock().is_empty()
|
||||
}
|
||||
|
||||
fn is_live_resized(id: u32) -> bool {
|
||||
LIVE_RESIZED_WINDOWS.lock().contains(&id)
|
||||
}
|
||||
|
||||
@@ -120,6 +120,9 @@ pub struct LiveResize {
|
||||
pub resize_dimensions: Vec<Option<Rect>>,
|
||||
/// The workspace's [`Workspace::latest_layout`] when the drag began
|
||||
pub latest_layout: Vec<Rect>,
|
||||
/// The strip of a Scrolling workspace when the drag began: the widths of its columns, and
|
||||
/// where it is scrolled to, which every step starts from as well
|
||||
pub scrolling: Option<komorebi_scrolling::ScrollingSpace<String>>,
|
||||
/// whether the neighbours were moved while the drag was going on
|
||||
pub stepped: bool,
|
||||
}
|
||||
@@ -1019,8 +1022,13 @@ impl WindowManager {
|
||||
}
|
||||
Some(new_idx) => {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
workspace.swap_containers(origin_container_idx, new_idx);
|
||||
workspace.focus_container(new_idx);
|
||||
|
||||
// a column of the Scrolling layout moves whole, and its tiles move within it,
|
||||
// which is not a swap of two containers
|
||||
if !(workspace.is_scrolling() && workspace.move_scrolling(direction)) {
|
||||
workspace.swap_containers(origin_container_idx, new_idx);
|
||||
workspace.focus_container(new_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1883,6 +1891,16 @@ impl WindowManager {
|
||||
}
|
||||
WorkspaceLayer::Tiling => {
|
||||
match workspace.layout {
|
||||
Layout::Default(DefaultLayout::Scrolling) => {
|
||||
tracing::debug!("resizing column");
|
||||
workspace.resize_scrolling_column(direction, sizing, delta);
|
||||
|
||||
return if update {
|
||||
self.update_focused_workspace(false, false)
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
}
|
||||
Layout::Default(layout) => {
|
||||
tracing::debug!("resizing window");
|
||||
let len = NonZeroUsize::new(workspace.containers().len())
|
||||
@@ -2057,6 +2075,10 @@ impl WindowManager {
|
||||
// Reset any resize adjustments if we want to force a retile
|
||||
if !preserve_resize_dimensions {
|
||||
workspace.resize_dimensions.fill(None);
|
||||
|
||||
// and the widths and heights of the Scrolling layout, which are resize
|
||||
// adjustments too
|
||||
workspace.reset_scrolling_sizes();
|
||||
}
|
||||
|
||||
if (workspace.wallpaper.is_some() || monitor_wp.is_some())
|
||||
|
||||
+124
-45
@@ -11,6 +11,7 @@
|
||||
//! [`Workspace::work_area_offset_rules`]), and its layout options come from the workspace or from
|
||||
//! the defaults of its layout.
|
||||
|
||||
use crate::HIDDEN_WINDOWS;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::backend::backend;
|
||||
use crate::container::Container;
|
||||
@@ -39,6 +40,7 @@ use crate::window::should_act;
|
||||
use crate::window::should_act_individual;
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::OptionExt;
|
||||
use komorebi_scrolling::ScrollingSpace;
|
||||
use komorebi_themes::KomorebiTheme;
|
||||
use komorebi_themes::KomorebiThemeCustom;
|
||||
use serde::Deserialize;
|
||||
@@ -127,6 +129,17 @@ pub struct Workspace {
|
||||
/// container
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub initial_window_placement_rules: Option<InitialWindowPlacementRules>,
|
||||
/// The strip of columns that the Scrolling layout lays containers out in, which is made when
|
||||
/// the layout first tiles the workspace ([`Workspace::layout_scrolling`])
|
||||
#[serde(skip)]
|
||||
#[cfg_attr(feature = "schemars", schemars(skip))]
|
||||
pub(crate) scrolling: Option<ScrollingSpace<String>>,
|
||||
/// When the windows of a Scrolling workspace were last sent to where the strip puts them, and
|
||||
/// how wide each container was made to be, for telling when the sizes that they report can be
|
||||
/// believed ([`crate::scrolling::WIDTH_SETTLE`])
|
||||
#[serde(skip)]
|
||||
#[cfg_attr(feature = "schemars", schemars(skip))]
|
||||
pub(crate) scrolling_placed: Option<crate::scrolling::Placed>,
|
||||
}
|
||||
|
||||
/// Which of a workspace's two kinds of windows the focus, move and resize commands act on: the
|
||||
@@ -188,6 +201,8 @@ impl Default for Workspace {
|
||||
preselected_container_idx: None,
|
||||
initial_window_placement_rules: None,
|
||||
promotion_swap_container_idx: None,
|
||||
scrolling: None,
|
||||
scrolling_placed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,7 +425,7 @@ impl Workspace {
|
||||
/// 1. Try threshold match from rules (highest matching threshold wins)
|
||||
/// 2. If a rule matches -> use it (full replacement of base)
|
||||
/// 3. Else -> use the base `layout_options`
|
||||
fn effective_layout_options(&self) -> Option<LayoutOptions> {
|
||||
pub(crate) fn effective_layout_options(&self) -> Option<LayoutOptions> {
|
||||
let container_count = self.containers().len();
|
||||
|
||||
let has_workspace_overrides =
|
||||
@@ -462,6 +477,12 @@ impl Workspace {
|
||||
/// Returns the index of the container whose tile is on the `direction` side of the focused
|
||||
/// one in the workspace's layout, or `None` when there is none on that side
|
||||
pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option<usize> {
|
||||
// in a strip, left and right are the neighbouring columns and up and down are the tiles of
|
||||
// the column, which the strip is the one to say
|
||||
if self.is_scrolling() && self.scrolling.is_some() {
|
||||
return self.scrolling_idx_for_direction(direction);
|
||||
}
|
||||
|
||||
let len = NonZeroUsize::new(self.containers().len())?;
|
||||
|
||||
direction.destination(
|
||||
@@ -542,6 +563,8 @@ impl Workspace {
|
||||
next
|
||||
} else if self.containers().is_empty() {
|
||||
0
|
||||
} else if let Some(idx) = self.scrolling_insertion_idx() {
|
||||
idx
|
||||
} else {
|
||||
self.resolve_placement_index(window)
|
||||
};
|
||||
@@ -837,8 +860,12 @@ impl Workspace {
|
||||
.ok_or_eyre("there is no window")?;
|
||||
|
||||
if container.windows().is_empty() {
|
||||
let container_id = container.id.clone();
|
||||
self.remove_container_by_idx(container_idx);
|
||||
self.focus_previous_container();
|
||||
|
||||
if !self.focus_after_scrolling_removal(&container_id) {
|
||||
self.focus_previous_container();
|
||||
}
|
||||
} else {
|
||||
container.load_focused_window()?;
|
||||
if let Some(window) = container.focused_window() {
|
||||
@@ -852,6 +879,9 @@ impl Workspace {
|
||||
// this fn respects locked container indexes - we should use it for pretty much everything
|
||||
// except monocle and maximize toggles
|
||||
pub fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
|
||||
// wherever it goes next, it goes as wide as it was
|
||||
self.note_scrolling_departure(idx);
|
||||
|
||||
let container = self.containers_mut().remove_respecting_locks(idx);
|
||||
|
||||
if idx < self.resize_dimensions.len() {
|
||||
@@ -1826,6 +1856,22 @@ impl Workspace {
|
||||
// the compositor's own maximize, which is the work area and not the padded one
|
||||
window.maximize()?;
|
||||
} else if !self.containers().is_empty() {
|
||||
let is_scrolling = self.is_scrolling();
|
||||
let around_window = container_padding + border_offset + border_width;
|
||||
|
||||
// a window that would not get as narrow as its column was learned of once the
|
||||
// windows have had time to draw at the sizes that they were sent, and not
|
||||
// while one is being dragged, whose neighbours are on their way
|
||||
if is_scrolling
|
||||
&& self
|
||||
.scrolling_placed
|
||||
.as_ref()
|
||||
.is_some_and(crate::scrolling::Placed::has_settled)
|
||||
&& !crate::window::live_resize_in_progress()
|
||||
{
|
||||
self.adopt_actual_widths(around_window, adjusted_work_area.right);
|
||||
}
|
||||
|
||||
let effective_layout_options = self.effective_layout_options();
|
||||
|
||||
tracing::debug!(
|
||||
@@ -1835,31 +1881,55 @@ impl Workspace {
|
||||
self.layout_options,
|
||||
self.layout_options_rules.len(),
|
||||
);
|
||||
let mut layouts = self.layout.as_boxed_arrangement().calculate(
|
||||
&adjusted_work_area,
|
||||
NonZeroUsize::new(self.containers().len()).ok_or_eyre(
|
||||
"there must be at least one container to calculate a workspace layout",
|
||||
)?,
|
||||
Some(container_padding),
|
||||
self.layout_flip,
|
||||
&self.resize_dimensions,
|
||||
self.focused_container_idx(),
|
||||
effective_layout_options,
|
||||
&self.latest_layout,
|
||||
);
|
||||
// the containers that rest where another monitor is seen from, which are hidden
|
||||
let mut hidden = vec![];
|
||||
let mut layouts = if is_scrolling {
|
||||
let monitors: Vec<Rect> = backend()
|
||||
.monitors()
|
||||
.map(|monitors| monitors.iter().map(|monitor| monitor.size).collect())
|
||||
.unwrap_or_default();
|
||||
let others =
|
||||
crate::scrolling::other_monitors(&self.globals.work_area, &monitors);
|
||||
|
||||
self.layout_scrolling(&adjusted_work_area, container_padding)
|
||||
.into_iter()
|
||||
.map(|placement| {
|
||||
let (rect, hide) = crate::scrolling::rest(
|
||||
placement.rect,
|
||||
placement.visibility,
|
||||
&adjusted_work_area,
|
||||
&others,
|
||||
);
|
||||
hidden.push(hide);
|
||||
rect
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
self.layout.as_boxed_arrangement().calculate(
|
||||
&adjusted_work_area,
|
||||
NonZeroUsize::new(self.containers().len()).ok_or_eyre(
|
||||
"there must be at least one container to calculate a workspace layout",
|
||||
)?,
|
||||
Some(container_padding),
|
||||
self.layout_flip,
|
||||
&self.resize_dimensions,
|
||||
self.focused_container_idx(),
|
||||
effective_layout_options,
|
||||
&self.latest_layout,
|
||||
)
|
||||
};
|
||||
|
||||
let resize_dimensions_is_empty = self.resize_dimensions.is_empty();
|
||||
let is_scrolling =
|
||||
matches!(self.layout, Layout::Default(DefaultLayout::Scrolling));
|
||||
|
||||
let containers = self.containers_mut();
|
||||
|
||||
for (i, container) in containers.iter_mut().enumerate() {
|
||||
if let Some(layout) = layouts.get_mut(i) {
|
||||
// The scrolling layout is a strip that is meant to run past the edges of
|
||||
// the work area, with the windows outside the viewport sitting off screen
|
||||
// (KWin does not constrain where a window is put). Everything else is
|
||||
// clamped to the work area so it cannot bleed onto an adjacent monitor.
|
||||
// the work area, and says itself where each window rests when it is out
|
||||
// of view (KWin does not constrain where a window is put). Everything
|
||||
// else is clamped to the work area so it cannot bleed onto an adjacent
|
||||
// monitor.
|
||||
if !is_scrolling {
|
||||
// horizontal
|
||||
if layout.left < adjusted_work_area.left {
|
||||
@@ -1889,6 +1959,29 @@ impl Workspace {
|
||||
layout.add_padding(border_offset);
|
||||
layout.add_padding(border_width);
|
||||
|
||||
if hidden.get(i).copied().unwrap_or_default() {
|
||||
// it is seen on another monitor wherever it rests here
|
||||
for window in container.windows() {
|
||||
window.hide();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// a window that was hidden for resting past an edge with another
|
||||
// monitor next to it is shown before it is put where the strip says
|
||||
let front = container.focused_window().map(|window| window.id);
|
||||
let restore = is_scrolling
|
||||
&& front.is_some_and(|front| {
|
||||
crate::scrolling::should_restore_when_placed(
|
||||
front,
|
||||
Some(front),
|
||||
&HIDDEN_WINDOWS.lock(),
|
||||
)
|
||||
});
|
||||
if restore && let Some(window) = container.focused_window() {
|
||||
window.restore();
|
||||
}
|
||||
|
||||
for window in container.windows_mut() {
|
||||
let current_rect = window.rect().unwrap_or_default();
|
||||
let current_percentage =
|
||||
@@ -1896,7 +1989,9 @@ impl Workspace {
|
||||
let proposed_percentage =
|
||||
work_area.percentage_within_horizontal_bounds(layout);
|
||||
|
||||
let percentage_override = proposed_percentage == 0.0
|
||||
// the strip puts a window where it goes, however far
|
||||
let percentage_override = !is_scrolling
|
||||
&& proposed_percentage == 0.0
|
||||
&& current_percentage > 0.1
|
||||
&& current_percentage < 100.0
|
||||
&& resize_dimensions_is_empty;
|
||||
@@ -1911,6 +2006,15 @@ impl Workspace {
|
||||
}
|
||||
|
||||
self.latest_layout = layouts;
|
||||
|
||||
if is_scrolling {
|
||||
self.scrolling_placed = Some(crate::scrolling::Placed::new(
|
||||
self.containers()
|
||||
.iter()
|
||||
.zip(self.latest_layout.iter())
|
||||
.map(|(container, layout)| (container.id.clone(), layout.right)),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
@@ -2078,9 +2182,6 @@ impl Workspace {
|
||||
Layout::Default(DefaultLayout::UltrawideVerticalStack) => {
|
||||
self.enforce_resize_for_ultrawide();
|
||||
}
|
||||
Layout::Default(DefaultLayout::Scrolling) => {
|
||||
self.enforce_resize_for_scrolling();
|
||||
}
|
||||
_ => self.enforce_no_resize(),
|
||||
}
|
||||
}
|
||||
@@ -2308,28 +2409,6 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_resize_for_scrolling(&mut self) {
|
||||
let resize_dimensions = &mut self.resize_dimensions;
|
||||
match resize_dimensions.len() {
|
||||
0 | 1 => self.enforce_no_resize(),
|
||||
_ => {
|
||||
let len = resize_dimensions.len();
|
||||
|
||||
for (i, rect) in resize_dimensions.iter_mut().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
rect.top = 0;
|
||||
rect.bottom = 0;
|
||||
|
||||
if i == 0 {
|
||||
rect.left = 0;
|
||||
} else if i == len - 1 {
|
||||
rect.right = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn enforce_no_resize(&mut self) {
|
||||
for rect in self.resize_dimensions.iter_mut().flatten() {
|
||||
rect.left = 0;
|
||||
|
||||
@@ -71,8 +71,10 @@ fn in_view(rect: Rect) -> bool {
|
||||
rect.left >= 0 && rect.left + rect.right <= 1920
|
||||
}
|
||||
|
||||
/// A window out of view rests at the edge of the work area that it left by, with a few pixels of
|
||||
/// it still on the screen
|
||||
fn out_of_view(rect: Rect) -> bool {
|
||||
rect.left >= 1920 || rect.left + rect.right <= 0
|
||||
rect.left >= 1920 - 30 || rect.left + rect.right <= 30
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -139,13 +141,18 @@ fn the_scrolling_strip_runs_past_the_edge_of_the_work_area_with_the_focused_wind
|
||||
"the first three are scrolled out of view: {rects:?}"
|
||||
);
|
||||
}
|
||||
// not clamped: every column is as wide as the others, at the same distance from the next
|
||||
let pitch = rects[4].left - rects[3].left;
|
||||
assert_eq!(rects[0].left, rects[3].left - 3 * pitch, "{rects:?}");
|
||||
// not clamped: every column is as wide as the others, and the ones out of view keep their
|
||||
// size and rest against the edge that they left by
|
||||
assert!(
|
||||
rects.iter().all(|rect| rect.right == rects[0].right),
|
||||
"{rects:?}"
|
||||
);
|
||||
for rect in &rects[..3] {
|
||||
assert!(
|
||||
rect.left + rect.right > 0 && rect.left + rect.right <= 30,
|
||||
"a few pixels of it are on the screen: {rects:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -187,17 +194,30 @@ fn scrolling_layout_columns_sets_how_many_columns_are_in_view() {
|
||||
for id in 22_031..=22_034 {
|
||||
open(&fake, &mut wm, id);
|
||||
}
|
||||
let width = |id: u32| fake.rect_of(id).unwrap().right;
|
||||
let three = width(22_031);
|
||||
|
||||
// a column is as wide as a share of the screen: a quarter of it is narrower than a third
|
||||
send(&fake, &mut wm, columns(4));
|
||||
let four: Vec<Rect> = (22_031..=22_034)
|
||||
.map(|id| fake.rect_of(id).unwrap())
|
||||
.collect();
|
||||
assert!(four.iter().all(|rect| in_view(*rect)), "{four:?}");
|
||||
assert!(four.iter().all(|rect| rect.right < three), "{four:?}");
|
||||
assert!(
|
||||
four.iter().all(|rect| rect.right * 4 <= 1920),
|
||||
"four of them fit across the screen: {four:?}"
|
||||
);
|
||||
assert!(
|
||||
four.iter()
|
||||
.all(|rect| (rect.right - four[0].right).abs() <= 1),
|
||||
"and they are as wide as each other: {four:?}"
|
||||
);
|
||||
|
||||
send(&fake, &mut wm, columns(1));
|
||||
let one: Vec<Rect> = (22_031..=22_034)
|
||||
.map(|id| fake.rect_of(id).unwrap())
|
||||
.collect();
|
||||
assert!(one[3].right > 1800, "one fills the screen: {one:?}");
|
||||
assert_eq!(
|
||||
one.iter().filter(|rect| in_view(**rect)).count(),
|
||||
1,
|
||||
|
||||
+401
-7
@@ -1,14 +1,18 @@
|
||||
//! The scrolling layout, against an in-memory desktop.
|
||||
//! The scrolling layout, against an in-memory desktop: columns of several windows and the commands
|
||||
//! that make them, the width and the place of a column, panning the view, where a window that is
|
||||
//! out of view rests, and what a drag of the edge of a column does.
|
||||
//!
|
||||
//! KWin lets a window sit anywhere, so the columns of a strip that are past the edge of a monitor
|
||||
//! can be on the next monitor over. Those windows still belong to the monitor whose strip they are
|
||||
//! in.
|
||||
//! can be on the next monitor over. Those windows are hidden, and still belong to the monitor whose
|
||||
//! strip they are in.
|
||||
//!
|
||||
//! Window ids are in the 10_000s.
|
||||
|
||||
use komorebi::backend::fake::FakeBackend;
|
||||
use komorebi::backend::with_backend;
|
||||
use komorebi::core::DefaultLayout;
|
||||
use komorebi::core::OperationDirection;
|
||||
use komorebi::core::PanDirection;
|
||||
use komorebi::core::Rect;
|
||||
use komorebi::core::SocketMessage;
|
||||
use komorebi::window::Window;
|
||||
@@ -52,7 +56,7 @@ fn open(fake: &FakeBackend, id: u32) {
|
||||
#[test]
|
||||
fn a_scrolling_strip_on_the_next_monitor_over_does_not_take_focus_with_it() {
|
||||
// KWin lets a window sit anywhere, so the part of the strip past the right edge of the first
|
||||
// monitor is on the second one. Those windows still belong to the first.
|
||||
// monitor would be on the second one. Those windows are hidden, and still belong to the first.
|
||||
let fake = FakeBackend::with_one_monitor();
|
||||
fake.add_monitor(
|
||||
2,
|
||||
@@ -98,10 +102,12 @@ fn a_scrolling_strip_on_the_next_monitor_over_does_not_take_focus_with_it() {
|
||||
// (the compositor's focus event is what scrolls the strip, and the fake sends none)
|
||||
wm.update_focused_workspace(false, false)?;
|
||||
|
||||
let past_the_edge = fake.rect_of(10_204).unwrap();
|
||||
// the part of the strip past the edge would be on the second monitor, so it is hidden
|
||||
// there and not left on the screen of another monitor
|
||||
assert!(
|
||||
past_the_edge.left >= 1920,
|
||||
"the test needs a window on the second monitor, got {past_the_edge:?}"
|
||||
fake.is_hidden(10_204),
|
||||
"the test needs a window past the edge, hidden: {:?}",
|
||||
fake.rect_of(10_204)
|
||||
);
|
||||
|
||||
wm.process_event(WindowManagerEvent::FocusChange(
|
||||
@@ -117,3 +123,391 @@ fn a_scrolling_strip_on_the_next_monitor_over_does_not_take_focus_with_it() {
|
||||
"focus moved to the monitor the strip spills onto"
|
||||
);
|
||||
}
|
||||
|
||||
fn send(fake: &Arc<FakeBackend>, wm: &mut WindowManager, message: SocketMessage) {
|
||||
with_backend(fake.clone(), || wm.process_command(message, Vec::new())).unwrap();
|
||||
}
|
||||
|
||||
/// A window manager on one monitor, the scrolling layout with `columns` columns in view, and
|
||||
/// windows `first..first + count`, the last of them focused
|
||||
fn strip(columns: usize, first: u32, count: u32) -> (Arc<FakeBackend>, WindowManager) {
|
||||
let (fake, mut wm) = desktop_on(FakeBackend::with_one_monitor());
|
||||
for id in first..first + count {
|
||||
open(&fake, id);
|
||||
}
|
||||
send(
|
||||
&fake,
|
||||
&mut wm,
|
||||
SocketMessage::ChangeLayout(DefaultLayout::Scrolling),
|
||||
);
|
||||
send(
|
||||
&fake,
|
||||
&mut wm,
|
||||
SocketMessage::ScrollingLayoutColumns(std::num::NonZeroUsize::new(columns).unwrap()),
|
||||
);
|
||||
with_backend(fake.clone(), || {
|
||||
for id in first..first + count {
|
||||
wm.process_event(added(id)).unwrap();
|
||||
}
|
||||
});
|
||||
(fake, wm)
|
||||
}
|
||||
|
||||
fn focus(fake: &Arc<FakeBackend>, wm: &mut WindowManager, direction: OperationDirection) {
|
||||
send(fake, wm, SocketMessage::FocusWindow(direction));
|
||||
// the compositor reports the focus it was asked to give, which is what scrolls the strip
|
||||
let id = wm.focused_window().unwrap().id;
|
||||
with_backend(fake.clone(), || {
|
||||
wm.process_event(WindowManagerEvent::FocusChange(
|
||||
SystemNotification::KWin(KWinNotification::WindowActivated),
|
||||
Window::from(id),
|
||||
))
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn rect(fake: &FakeBackend, id: u32) -> Rect {
|
||||
fake.rect_of(id).unwrap()
|
||||
}
|
||||
|
||||
fn in_view(rect: Rect) -> bool {
|
||||
rect.left >= 0 && rect.left + rect.right <= 1920
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consuming_puts_two_windows_in_one_column_and_expelling_takes_one_out() {
|
||||
let (fake, mut wm) = strip(3, 10_301, 3);
|
||||
focus(&fake, &mut wm, OperationDirection::Left);
|
||||
focus(&fake, &mut wm, OperationDirection::Left);
|
||||
assert_eq!(wm.focused_window().unwrap().id, 10_301);
|
||||
|
||||
send(
|
||||
&fake,
|
||||
&mut wm,
|
||||
SocketMessage::Consume(OperationDirection::Right),
|
||||
);
|
||||
|
||||
let (first, second, third) = (
|
||||
rect(&fake, 10_301),
|
||||
rect(&fake, 10_302),
|
||||
rect(&fake, 10_303),
|
||||
);
|
||||
assert_eq!(first.left, second.left, "one column: {first:?} {second:?}");
|
||||
assert_eq!(first.right, second.right);
|
||||
assert!(
|
||||
second.top >= first.top + first.bottom,
|
||||
"one above the other"
|
||||
);
|
||||
assert!(first.bottom < 600, "and each has a share of the height");
|
||||
assert_ne!(second.left, third.left);
|
||||
assert_eq!(wm.focused_window().unwrap().id, 10_301, "focus stays");
|
||||
|
||||
send(&fake, &mut wm, SocketMessage::Expel);
|
||||
|
||||
let (first, second) = (rect(&fake, 10_301), rect(&fake, 10_302));
|
||||
assert_ne!(first.left, second.left, "a column each again");
|
||||
assert_eq!(first.top, second.top);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_column_is_moved_and_focused_whole_and_its_windows_up_and_down() {
|
||||
let (fake, mut wm) = strip(3, 10_311, 3);
|
||||
focus(&fake, &mut wm, OperationDirection::Left);
|
||||
focus(&fake, &mut wm, OperationDirection::Left);
|
||||
send(
|
||||
&fake,
|
||||
&mut wm,
|
||||
SocketMessage::Consume(OperationDirection::Right),
|
||||
);
|
||||
|
||||
// right goes to the next column, and down to the other window of this one
|
||||
focus(&fake, &mut wm, OperationDirection::Down);
|
||||
assert_eq!(wm.focused_window().unwrap().id, 10_312);
|
||||
focus(&fake, &mut wm, OperationDirection::Up);
|
||||
assert_eq!(wm.focused_window().unwrap().id, 10_311);
|
||||
focus(&fake, &mut wm, OperationDirection::Right);
|
||||
assert_eq!(wm.focused_window().unwrap().id, 10_313);
|
||||
|
||||
// the column of two moves as one: the window of the column on the right is now before it
|
||||
focus(&fake, &mut wm, OperationDirection::Left);
|
||||
send(
|
||||
&fake,
|
||||
&mut wm,
|
||||
SocketMessage::MoveWindow(OperationDirection::Right),
|
||||
);
|
||||
assert!(rect(&fake, 10_313).left < rect(&fake, 10_311).left);
|
||||
assert_eq!(rect(&fake, 10_311).left, rect(&fake, 10_312).left);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggling_the_stack_of_a_column_stacks_its_windows_and_spreads_them_out_again() {
|
||||
let (fake, mut wm) = strip(3, 10_321, 3);
|
||||
focus(&fake, &mut wm, OperationDirection::Left);
|
||||
focus(&fake, &mut wm, OperationDirection::Left);
|
||||
send(
|
||||
&fake,
|
||||
&mut wm,
|
||||
SocketMessage::Consume(OperationDirection::Right),
|
||||
);
|
||||
|
||||
send(&fake, &mut wm, SocketMessage::ToggleColumnStack);
|
||||
assert_eq!(wm.focused_workspace().unwrap().containers().len(), 2);
|
||||
let shown = [10_321, 10_322]
|
||||
.into_iter()
|
||||
.filter(|id| !fake.is_hidden(*id))
|
||||
.count();
|
||||
assert_eq!(shown, 1, "one window of the stack shows at a time");
|
||||
assert_eq!(
|
||||
rect(&fake, 10_321).bottom,
|
||||
rect(&fake, 10_323).bottom,
|
||||
"and takes the height of the column"
|
||||
);
|
||||
|
||||
send(&fake, &mut wm, SocketMessage::ToggleColumnStack);
|
||||
assert_eq!(wm.focused_workspace().unwrap().containers().len(), 3);
|
||||
assert!(
|
||||
!fake.is_hidden(10_321) && !fake.is_hidden(10_322),
|
||||
"every window of a stack that is spread out is shown"
|
||||
);
|
||||
assert_eq!(rect(&fake, 10_321).left, rect(&fake, 10_322).left);
|
||||
assert_ne!(rect(&fake, 10_321).top, rect(&fake, 10_322).top);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_column_is_made_as_wide_as_the_screen_and_put_back() {
|
||||
let (fake, mut wm) = strip(3, 10_331, 2);
|
||||
let before = rect(&fake, 10_332);
|
||||
|
||||
send(&fake, &mut wm, SocketMessage::ScrollingLayoutFullWidth);
|
||||
let full = rect(&fake, 10_332);
|
||||
assert!(full.right > 1800, "{full:?}");
|
||||
assert!(in_view(full), "{full:?}");
|
||||
|
||||
send(&fake, &mut wm, SocketMessage::ScrollingLayoutFullWidth);
|
||||
assert!((rect(&fake, 10_332).right - before.right).abs() <= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expanding_fills_the_room_that_the_columns_in_view_leave() {
|
||||
let (fake, mut wm) = strip(3, 10_341, 2);
|
||||
let before = rect(&fake, 10_342);
|
||||
|
||||
send(&fake, &mut wm, SocketMessage::Expand);
|
||||
|
||||
let after = rect(&fake, 10_342);
|
||||
assert!(after.right > before.right + 400, "{before:?} {after:?}");
|
||||
assert_eq!(rect(&fake, 10_341).left, 10, "the column before it stays");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn centring_the_focus_puts_the_focused_column_in_the_middle_of_the_screen() {
|
||||
let (fake, mut wm) = strip(3, 10_351, 4);
|
||||
focus(&fake, &mut wm, OperationDirection::Left);
|
||||
focus(&fake, &mut wm, OperationDirection::Left);
|
||||
|
||||
send(&fake, &mut wm, SocketMessage::CenterFocus);
|
||||
|
||||
let focused = rect(&fake, wm.focused_window().unwrap().id);
|
||||
let middle = focused.left + focused.right / 2;
|
||||
assert!((middle - 960).abs() <= 12, "{focused:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panning_moves_the_view_a_column_and_focus_goes_to_a_column_in_view() {
|
||||
let (fake, mut wm) = strip(3, 10_361, 6);
|
||||
for _ in 0..5 {
|
||||
focus(&fake, &mut wm, OperationDirection::Left);
|
||||
}
|
||||
assert_eq!(wm.focused_window().unwrap().id, 10_361);
|
||||
let before = rect(&fake, 10_364);
|
||||
|
||||
send(&fake, &mut wm, SocketMessage::Pan(PanDirection::Right));
|
||||
|
||||
let after = rect(&fake, 10_364);
|
||||
assert!(
|
||||
after.left < before.left,
|
||||
"the view went right: {before:?} {after:?}"
|
||||
);
|
||||
let focused = wm.focused_window().unwrap().id;
|
||||
assert_ne!(focused, 10_361, "the first column is out of view");
|
||||
assert!(in_view(rect(&fake, focused)), "and focus is in view");
|
||||
|
||||
send(&fake, &mut wm, SocketMessage::Pan(PanDirection::Left));
|
||||
assert_eq!(rect(&fake, 10_364).left, before.left, "and back");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_that_scrolls_out_of_view_keeps_its_size_and_rests_at_the_edge() {
|
||||
let (fake, mut wm) = strip(2, 10_371, 4);
|
||||
let in_front = rect(&fake, 10_374);
|
||||
|
||||
let first = rect(&fake, 10_371);
|
||||
assert_eq!(first.right, in_front.right, "the size is the same");
|
||||
assert!(
|
||||
first.left + first.right > 0 && first.left + first.right <= 30,
|
||||
"a few pixels of it are on the screen: {first:?}"
|
||||
);
|
||||
assert!(
|
||||
!fake.is_hidden(10_371),
|
||||
"with no monitor next to it, it is not hidden"
|
||||
);
|
||||
|
||||
// and it comes back where the strip puts it when the focus goes to it
|
||||
for _ in 0..3 {
|
||||
focus(&fake, &mut wm, OperationDirection::Left);
|
||||
}
|
||||
with_backend(fake.clone(), || {
|
||||
wm.process_event(WindowManagerEvent::FocusChange(
|
||||
SystemNotification::KWin(KWinNotification::WindowActivated),
|
||||
Window::from(10_371),
|
||||
))
|
||||
.unwrap();
|
||||
});
|
||||
assert!(in_view(rect(&fake, 10_371)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_that_is_hidden_past_the_edge_of_a_monitor_is_shown_where_the_strip_scrolls_to_it() {
|
||||
let fake = FakeBackend::with_one_monitor();
|
||||
fake.add_monitor(
|
||||
2,
|
||||
"Virtual-1",
|
||||
Rect {
|
||||
left: 1920,
|
||||
top: 0,
|
||||
right: 1920,
|
||||
bottom: 1080,
|
||||
},
|
||||
Rect {
|
||||
left: 1920,
|
||||
top: 0,
|
||||
right: 1920,
|
||||
bottom: 1050,
|
||||
},
|
||||
);
|
||||
let (fake, mut wm) = desktop_on(fake);
|
||||
for id in 10_381..=10_384 {
|
||||
open(&fake, id);
|
||||
}
|
||||
send(
|
||||
&fake,
|
||||
&mut wm,
|
||||
SocketMessage::ChangeLayout(DefaultLayout::Scrolling),
|
||||
);
|
||||
send(
|
||||
&fake,
|
||||
&mut wm,
|
||||
SocketMessage::ScrollingLayoutColumns(std::num::NonZeroUsize::new(2).unwrap()),
|
||||
);
|
||||
with_backend(fake.clone(), || {
|
||||
for id in 10_381..=10_384 {
|
||||
wm.process_event(added(id)).unwrap();
|
||||
}
|
||||
});
|
||||
for _ in 0..3 {
|
||||
focus(&fake, &mut wm, OperationDirection::Left);
|
||||
}
|
||||
assert!(
|
||||
fake.is_hidden(10_384),
|
||||
"past the right edge, next to a monitor"
|
||||
);
|
||||
assert!(!fake.is_hidden(10_381));
|
||||
|
||||
// a window that rests or overhangs past an edge with a monitor next to it is hidden, so no
|
||||
// window of the strip reaches the other monitor
|
||||
send(&fake, &mut wm, SocketMessage::Pan(PanDirection::Right));
|
||||
for id in 10_381..=10_384 {
|
||||
let rect = rect(&fake, id);
|
||||
assert!(
|
||||
fake.is_hidden(id) || rect.left + rect.right <= 1920,
|
||||
"{id} reaches the monitor next to it: {rect:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// and it comes back, shown, when the focus goes to it
|
||||
for _ in 0..2 {
|
||||
focus(&fake, &mut wm, OperationDirection::Right);
|
||||
}
|
||||
assert_eq!(wm.focused_window().unwrap().id, 10_384);
|
||||
assert!(!fake.is_hidden(10_384));
|
||||
assert!(in_view(rect(&fake, 10_384)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_that_will_not_get_as_narrow_widens_its_column_and_the_rest_make_room() {
|
||||
let (fake, mut wm) = desktop_on(FakeBackend::with_one_monitor());
|
||||
for id in 10_391..=10_393 {
|
||||
open(&fake, id);
|
||||
}
|
||||
// a client with a minimum width, as a GTK 4 application or Firefox has
|
||||
fake.set_min_size(10_392, 1300, 100);
|
||||
send(
|
||||
&fake,
|
||||
&mut wm,
|
||||
SocketMessage::ChangeLayout(DefaultLayout::Scrolling),
|
||||
);
|
||||
with_backend(fake.clone(), || {
|
||||
for id in 10_391..=10_393 {
|
||||
wm.process_event(added(id)).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
let (wide, after) = (rect(&fake, 10_392), rect(&fake, 10_393));
|
||||
assert!(wide.right >= 1300, "{wide:?}");
|
||||
assert!(
|
||||
after.left < wide.left + wide.right,
|
||||
"until it is learned, the window covers its neighbour: {wide:?} {after:?}"
|
||||
);
|
||||
|
||||
// the windows have had time to draw at the sizes they were sent, and the next layout learns
|
||||
// what they are
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
send(
|
||||
&fake,
|
||||
&mut wm,
|
||||
SocketMessage::ScrollingLayoutColumns(std::num::NonZeroUsize::new(3).unwrap()),
|
||||
);
|
||||
|
||||
let (wide, after) = (rect(&fake, 10_392), rect(&fake, 10_393));
|
||||
assert!(wide.right >= 1300, "{wide:?}");
|
||||
assert!(
|
||||
after.left >= wide.left + wide.right,
|
||||
"nothing overlaps now: {wide:?} {after:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_left_edge_of_a_column_is_where_it_is_dragged_to() {
|
||||
let (fake, mut wm) = strip(3, 10_401, 3);
|
||||
let before = rect(&fake, 10_403);
|
||||
|
||||
with_backend(fake.clone(), || {
|
||||
wm.process_event(WindowManagerEvent::ResizeStart(
|
||||
SystemNotification::KWin(KWinNotification::InteractiveResizeStarted),
|
||||
Window::from(10_403),
|
||||
))
|
||||
.unwrap();
|
||||
let dragged = Rect {
|
||||
left: before.left - 100,
|
||||
right: before.right + 100,
|
||||
..before
|
||||
};
|
||||
fake.move_window_by_user(10_403, dragged);
|
||||
wm.process_event(WindowManagerEvent::ResizeEnd(
|
||||
SystemNotification::KWin(KWinNotification::InteractiveResizeFinished),
|
||||
Window::from(10_403),
|
||||
))
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let after = rect(&fake, 10_403);
|
||||
// to the pixel, give or take what a width that is not a whole number of them rounds to
|
||||
assert!(
|
||||
(after.left - (before.left - 100)).abs() <= 2,
|
||||
"{before:?} {after:?}"
|
||||
);
|
||||
assert!(
|
||||
((after.left + after.right) - (before.left + before.right)).abs() <= 2,
|
||||
"the right edge stays: {before:?} {after:?}"
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,10 @@
|
||||
//! workspace, `scrolling-layout-columns` and `layout-ratios` tune it, `toggle-tiling` turns its
|
||||
//! tiling off and on, and `preselect-direction` and `cancel-preselect` reserve the place of the
|
||||
//! next window and give it up.
|
||||
//! - `consume`, `expel` and `toggle-column-stack` put windows in the same column of the Scrolling
|
||||
//! layout and take them out, `scrolling-layout-full-width`, `center-focus` and `expand` make the
|
||||
//! focused column as wide as the screen, put it in the middle and fill the room that is left,
|
||||
//! and `pan` moves the view along the strip without moving the focus.
|
||||
//! - `stack` puts the focused window in one container with the window on one side of it, so that
|
||||
//! they share a tile and only one is in sight, `unstack` takes it out again, and `stack-all` and
|
||||
//! `unstack-all` do it for the whole workspace and for the focused stack. `cycle-stack` brings
|
||||
@@ -130,6 +134,7 @@ use komorebi_client::FocusFollowsMouseImplementation;
|
||||
use komorebi_client::MoveBehaviour;
|
||||
use komorebi_client::OperationBehaviour;
|
||||
use komorebi_client::OperationDirection;
|
||||
use komorebi_client::PanDirection;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::Rect;
|
||||
use komorebi_client::Sizing;
|
||||
@@ -207,6 +212,7 @@ macro_rules! gen_enum_subcommand_args {
|
||||
gen_enum_subcommand_args! {
|
||||
Focus: OperationDirection,
|
||||
Move: OperationDirection,
|
||||
Consume: OperationDirection,
|
||||
PreselectDirection: OperationDirection,
|
||||
CycleFocus: CycleDirection,
|
||||
CycleMove: CycleDirection,
|
||||
@@ -732,6 +738,13 @@ struct ScrollingLayoutColumns {
|
||||
count: NonZeroUsize,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Pan {
|
||||
/// The way that the view goes
|
||||
#[clap(value_enum)]
|
||||
direction: PanDirection,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct LayoutRatios {
|
||||
/// Column width ratios (space-separated values between 0.1 and 0.9)
|
||||
@@ -1097,6 +1110,27 @@ enum SubCommand {
|
||||
/// Set the number of visible columns for the Scrolling layout on the focused workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
ScrollingLayoutColumns(ScrollingLayoutColumns),
|
||||
/// Make the focused column of the Scrolling layout as wide as the screen, or put it back to
|
||||
/// the width that it had
|
||||
ScrollingLayoutFullWidth,
|
||||
/// Scroll the Scrolling layout so that the focused column is in the middle of the screen, once
|
||||
CenterFocus,
|
||||
/// Make the focused column of the Scrolling layout fill the room that the columns in view leave
|
||||
/// on the screen, which does nothing in any other layout
|
||||
Expand,
|
||||
/// Take the neighbouring window on one side into the focused column of the Scrolling layout
|
||||
#[clap(arg_required_else_help = true)]
|
||||
Consume(Consume),
|
||||
/// Take the focused window out of its column in the Scrolling layout and put it in a column
|
||||
/// of its own on the right
|
||||
Expel,
|
||||
/// Stack the windows of the focused column of the Scrolling layout, or spread the focused
|
||||
/// stack out into a column with a window in each slot
|
||||
ToggleColumnStack,
|
||||
/// Move the view of the Scrolling layout a column to the left or the right, which does
|
||||
/// nothing in any other layout
|
||||
#[clap(arg_required_else_help = true)]
|
||||
Pan(Pan),
|
||||
/// Set the layout column and row ratios for the focused workspace
|
||||
LayoutRatios(LayoutRatios),
|
||||
// /// Load a custom layout from file for the focused workspace
|
||||
@@ -2049,6 +2083,27 @@ fn main() -> eyre::Result<()> {
|
||||
SubCommand::ScrollingLayoutColumns(args) => {
|
||||
send_message(&SocketMessage::ScrollingLayoutColumns(args.count))?;
|
||||
}
|
||||
SubCommand::ScrollingLayoutFullWidth => {
|
||||
send_message(&SocketMessage::ScrollingLayoutFullWidth)?;
|
||||
}
|
||||
SubCommand::CenterFocus => {
|
||||
send_message(&SocketMessage::CenterFocus)?;
|
||||
}
|
||||
SubCommand::Expand => {
|
||||
send_message(&SocketMessage::Expand)?;
|
||||
}
|
||||
SubCommand::Consume(args) => {
|
||||
send_message(&SocketMessage::Consume(args.operation_direction))?;
|
||||
}
|
||||
SubCommand::Expel => {
|
||||
send_message(&SocketMessage::Expel)?;
|
||||
}
|
||||
SubCommand::ToggleColumnStack => {
|
||||
send_message(&SocketMessage::ToggleColumnStack)?;
|
||||
}
|
||||
SubCommand::Pan(args) => {
|
||||
send_message(&SocketMessage::Pan(args.direction))?;
|
||||
}
|
||||
SubCommand::LayoutRatios(args) => {
|
||||
if args.columns.is_none() && args.rows.is_none() {
|
||||
println!(
|
||||
|
||||
@@ -311,6 +311,33 @@ fn scrolling_layout_columns_and_layout_ratios_send_what_they_are_given() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_commands_of_the_columns_of_the_scrolling_layout_send_their_message() {
|
||||
sends(
|
||||
&["scrolling-layout-full-width"],
|
||||
r#"{"type":"ScrollingLayoutFullWidth"}"#,
|
||||
);
|
||||
sends(&["center-focus"], r#"{"type":"CenterFocus"}"#);
|
||||
sends(&["expand"], r#"{"type":"Expand"}"#);
|
||||
sends(
|
||||
&["consume", "right"],
|
||||
r#"{"type":"Consume","content":"Right"}"#,
|
||||
);
|
||||
sends(&["expel"], r#"{"type":"Expel"}"#);
|
||||
sends(&["toggle-column-stack"], r#"{"type":"ToggleColumnStack"}"#);
|
||||
sends(&["pan", "left"], r#"{"type":"Pan","content":"Left"}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pan_and_consume_want_a_direction() {
|
||||
let runtime = Runtime::new();
|
||||
|
||||
for arguments in [&["pan"][..], &["pan", "up"], &["consume"]] {
|
||||
let output = runtime.komorebic(arguments);
|
||||
assert!(!output.status.success(), "{arguments:?}: {output:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_ratios_without_ratios_sends_nothing_and_says_so() {
|
||||
let runtime = Runtime::new();
|
||||
@@ -1118,6 +1145,7 @@ fn docgen_writes_the_help_of_every_subcommand_in_a_text_fence() {
|
||||
"border-width.md",
|
||||
"border.md",
|
||||
"cancel-preselect.md",
|
||||
"center-focus.md",
|
||||
"change-layout.md",
|
||||
"check.md",
|
||||
"clear-all-workspace-rules.md",
|
||||
@@ -1130,6 +1158,7 @@ fn docgen_writes_the_help_of_every_subcommand_in_a_text_fence() {
|
||||
"close.md",
|
||||
"completions.md",
|
||||
"configuration.md",
|
||||
"consume.md",
|
||||
"container-padding.md",
|
||||
"cross-monitor-move-behaviour.md",
|
||||
"cycle-empty-workspace.md",
|
||||
@@ -1153,6 +1182,8 @@ fn docgen_writes_the_help_of_every_subcommand_in_a_text_fence() {
|
||||
"enforce-workspace-rules.md",
|
||||
"ensure-named-workspaces.md",
|
||||
"ensure-workspaces.md",
|
||||
"expand.md",
|
||||
"expel.md",
|
||||
"export-kde-shortcuts.md",
|
||||
"flip-layout.md",
|
||||
"focus-last-workspace.md",
|
||||
@@ -1202,6 +1233,7 @@ fn docgen_writes_the_help_of_every_subcommand_in_a_text_fence() {
|
||||
"named-workspace-tiling.md",
|
||||
"new-workspace.md",
|
||||
"notification-schema.md",
|
||||
"pan.md",
|
||||
"preselect-direction.md",
|
||||
"promote-focus.md",
|
||||
"promote-swap.md",
|
||||
@@ -1218,6 +1250,7 @@ fn docgen_writes_the_help_of_every_subcommand_in_a_text_fence() {
|
||||
"retile.md",
|
||||
"save-resize.md",
|
||||
"scrolling-layout-columns.md",
|
||||
"scrolling-layout-full-width.md",
|
||||
"send-to-last-workspace.md",
|
||||
"send-to-monitor-workspace.md",
|
||||
"send-to-monitor.md",
|
||||
@@ -1234,6 +1267,7 @@ fn docgen_writes_the_help_of_every_subcommand_in_a_text_fence() {
|
||||
"stop.md",
|
||||
"subscribe-socket.md",
|
||||
"swap-workspaces-with-monitor.md",
|
||||
"toggle-column-stack.md",
|
||||
"toggle-cross-monitor-move-behaviour.md",
|
||||
"toggle-float-override.md",
|
||||
"toggle-float.md",
|
||||
|
||||
Reference in new issue
Block a user