feat: make scrolling the active default layout
This commit is contained in:
1 parent
8e4283bf16
commit
9d90415e55
13 files changed
+915
-56
No files matched your search
Generated
+51
-3
@@ -420,7 +420,9 @@ dependencies = [
|
|||||||
"ferese-core",
|
"ferese-core",
|
||||||
"ferese-layout",
|
"ferese-layout",
|
||||||
"libc",
|
"libc",
|
||||||
|
"serde",
|
||||||
"smithay",
|
"smithay",
|
||||||
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"xcursor",
|
"xcursor",
|
||||||
@@ -1393,6 +1395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1415,6 +1418,15 @@ dependencies = [
|
|||||||
"syn 3.0.6",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -1633,6 +1645,30 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.9.12+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime 0.7.5+spec-1.1.0",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.7.5+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.1.1+spec-1.1.0"
|
version = "1.1.1+spec-1.1.0"
|
||||||
@@ -1649,9 +1685,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
|
checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime",
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow",
|
"winnow 1.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1660,9 +1696,15 @@ version = "1.1.3+spec-1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow 1.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.44"
|
version = "0.1.44"
|
||||||
@@ -2231,6 +2273,12 @@ dependencies = [
|
|||||||
"xkbcommon-dl",
|
"xkbcommon-dl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|||||||
@@ -28,4 +28,6 @@ smithay = { version = "0.7.0", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
toml = "0.9.7"
|
||||||
xcursor = "0.3.11"
|
xcursor = "0.3.11"
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
# Ferese
|
# Ferese
|
||||||
|
|
||||||
Ferese is an experimental Wayland scrolling compositor written in Rust with
|
Ferese is an experimental Wayland scrolling compositor written in Rust with
|
||||||
Smithay. The current implementation includes the original tree layout,
|
Smithay. Scrolling columns are the default, tree tiling remains available as an
|
||||||
workspaces, and timestamp-driven animated geometry. The specification now makes
|
optional per-workspace mode, and geometry changes use timestamp-driven
|
||||||
scrolling columns the default and retains tree tiling as an optional mode. DRM
|
animation. See the
|
||||||
multi-output and native interoperability are being completed before that layout
|
[window-manager specification](docs/ferese-window-manager-spec.md) for the
|
||||||
transition in the
|
current architecture and milestones.
|
||||||
[window-manager specification](docs/ferese-window-manager-spec.md).
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -25,6 +24,19 @@ cargo run -p ferese -- foot
|
|||||||
|
|
||||||
Set `RUST_LOG=ferese=debug` for detailed compositor logging.
|
Set `RUST_LOG=ferese=debug` for detailed compositor logging.
|
||||||
|
|
||||||
|
Ferese reads `$XDG_CONFIG_HOME/ferese/config.toml`, or
|
||||||
|
`~/.config/ferese/config.toml` when `XDG_CONFIG_HOME` is unset. New scrolling
|
||||||
|
columns use half the viewport by default. To make every newly opened column use
|
||||||
|
the full viewport width:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[scrolling]
|
||||||
|
default_column_width = "full"
|
||||||
|
```
|
||||||
|
|
||||||
|
`default_column_width = 1.0` is equivalent. `Super+M` switches the active
|
||||||
|
workspace between scrolling and tree modes.
|
||||||
|
|
||||||
Text-input-v3 is available to applications by default. Input-method-v2 is
|
Text-input-v3 is available to applications by default. Input-method-v2 is
|
||||||
hidden unless the session explicitly opts in, because an input method can
|
hidden unless the session explicitly opts in, because an input method can
|
||||||
request privileged keyboard access. Enable it only when launching a trusted
|
request privileged keyboard access. Enable it only when launching a trusted
|
||||||
@@ -68,5 +80,6 @@ M4 validation requires the direct session to:
|
|||||||
- log page-flip timing and any missed presentation deadlines on the reference
|
- log page-flip timing and any missed presentation deadlines on the reference
|
||||||
integrated GPU.
|
integrated GPU.
|
||||||
|
|
||||||
The direct path currently targets one connected output. Multi-output hotplug is
|
The direct path creates an independent render pipeline per connected output and
|
||||||
part of M5.
|
rescans connectors on hotplug. Physical unplug/replug acceptance testing on the
|
||||||
|
reference hardware is still pending.
|
||||||
@@ -11,6 +11,8 @@ ferese-core = { path = "../crates/ferese-core" }
|
|||||||
ferese-layout = { path = "../crates/ferese-layout" }
|
ferese-layout = { path = "../crates/ferese-layout" }
|
||||||
libc.workspace = true
|
libc.workspace = true
|
||||||
smithay.workspace = true
|
smithay.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
toml.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
xcursor.workspace = true
|
xcursor.workspace = true
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
|
use std::fs;
|
||||||
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use ferese_core::LayoutMode;
|
||||||
|
use ferese_layout::ColumnWidth;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
#[serde(default)]
|
||||||
|
layout: LayoutConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
scrolling: ScrollingConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
struct LayoutConfig {
|
||||||
|
mode: Option<LayoutModeValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum LayoutModeValue {
|
||||||
|
Scrolling,
|
||||||
|
Tree,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
struct ScrollingConfig {
|
||||||
|
default_column_width: Option<ColumnWidthValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum ColumnWidthValue {
|
||||||
|
Proportion(f64),
|
||||||
|
Named(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
Read {
|
||||||
|
path: PathBuf,
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
|
Parse {
|
||||||
|
path: PathBuf,
|
||||||
|
source: toml::de::Error,
|
||||||
|
},
|
||||||
|
InvalidColumnWidth(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ConfigError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Read { path, source } => {
|
||||||
|
write!(formatter, "failed to read {}: {source}", path.display())
|
||||||
|
}
|
||||||
|
Self::Parse { path, source } => {
|
||||||
|
write!(formatter, "failed to parse {}: {source}", path.display())
|
||||||
|
}
|
||||||
|
Self::InvalidColumnWidth(value) => write!(
|
||||||
|
formatter,
|
||||||
|
"invalid scrolling.default_column_width {value}; expected a positive number or \"full\""
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for ConfigError {
|
||||||
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::Read { source, .. } => Some(source),
|
||||||
|
Self::Parse { source, .. } => Some(source),
|
||||||
|
Self::InvalidColumnWidth(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn load() -> Result<Self, ConfigError> {
|
||||||
|
let Some(path) = config_path() else {
|
||||||
|
return Ok(Self::default());
|
||||||
|
};
|
||||||
|
let source = match fs::read_to_string(&path) {
|
||||||
|
Ok(source) => source,
|
||||||
|
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Self::default()),
|
||||||
|
Err(source) => return Err(ConfigError::Read { path, source }),
|
||||||
|
};
|
||||||
|
|
||||||
|
toml::from_str(&source).map_err(|source| ConfigError::Parse { path, source })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layout_mode(&self) -> LayoutMode {
|
||||||
|
match self.layout.mode.unwrap_or(LayoutModeValue::Scrolling) {
|
||||||
|
LayoutModeValue::Scrolling => LayoutMode::Scrolling,
|
||||||
|
LayoutModeValue::Tree => LayoutMode::Tree,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_column_width(&self) -> Result<ColumnWidth, ConfigError> {
|
||||||
|
match self
|
||||||
|
.scrolling
|
||||||
|
.default_column_width
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&ColumnWidthValue::Proportion(0.5))
|
||||||
|
{
|
||||||
|
ColumnWidthValue::Proportion(value) if value.is_finite() && *value > 0.0 => {
|
||||||
|
Ok(ColumnWidth::Proportion(*value))
|
||||||
|
}
|
||||||
|
ColumnWidthValue::Named(value) if value.eq_ignore_ascii_case("full") => {
|
||||||
|
Ok(ColumnWidth::Full)
|
||||||
|
}
|
||||||
|
value => Err(ConfigError::InvalidColumnWidth(match value {
|
||||||
|
ColumnWidthValue::Proportion(value) => value.to_string(),
|
||||||
|
ColumnWidthValue::Named(value) => format!("{value:?}"),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_path() -> Option<PathBuf> {
|
||||||
|
if let Some(directory) = env::var_os("XDG_CONFIG_HOME").filter(|value| !value.is_empty()) {
|
||||||
|
return Some(PathBuf::from(directory).join("ferese/config.toml"));
|
||||||
|
}
|
||||||
|
|
||||||
|
env::var_os("HOME")
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.map(|home| home.join(".config/ferese/config.toml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn parse(source: &str) -> Config {
|
||||||
|
toml::from_str(source).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn defaults_to_half_width_scrolling_columns() {
|
||||||
|
let config = parse("");
|
||||||
|
|
||||||
|
assert_eq!(config.layout_mode(), LayoutMode::Scrolling);
|
||||||
|
assert_eq!(
|
||||||
|
config.default_column_width().unwrap(),
|
||||||
|
ColumnWidth::Proportion(0.5)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_full_and_numeric_column_widths() {
|
||||||
|
let full = parse("[scrolling]\ndefault_column_width = \"full\"");
|
||||||
|
let numeric = parse("[scrolling]\ndefault_column_width = 1.0");
|
||||||
|
|
||||||
|
assert_eq!(full.default_column_width().unwrap(), ColumnWidth::Full);
|
||||||
|
assert_eq!(
|
||||||
|
numeric.default_column_width().unwrap(),
|
||||||
|
ColumnWidth::Proportion(1.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_non_positive_or_unknown_column_widths() {
|
||||||
|
let zero = parse("[scrolling]\ndefault_column_width = 0.0");
|
||||||
|
let unknown = parse("[scrolling]\ndefault_column_width = \"wide\"");
|
||||||
|
|
||||||
|
assert!(zero.default_column_width().is_err());
|
||||||
|
assert!(unknown.default_column_width().is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,6 +138,15 @@ impl Ferese {
|
|||||||
data.toggle_focused_fullscreen();
|
data.toggle_focused_fullscreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FilterResult::Intercept(())
|
||||||
|
} else if !modifiers.shift
|
||||||
|
&& matches!(symbol, keysyms::KEY_m | keysyms::KEY_M)
|
||||||
|
{
|
||||||
|
if state == KeyState::Pressed {
|
||||||
|
data.intercepted_keys.insert(keycode);
|
||||||
|
data.toggle_layout_mode();
|
||||||
|
}
|
||||||
|
|
||||||
FilterResult::Intercept(())
|
FilterResult::Intercept(())
|
||||||
} else if modifiers.shift && symbol == keysyms::KEY_space {
|
} else if modifiers.shift && symbol == keysyms::KEY_space {
|
||||||
if state == KeyState::Pressed {
|
if state == KeyState::Pressed {
|
||||||
@@ -313,6 +322,8 @@ impl Ferese {
|
|||||||
toplevel.send_pending_configure();
|
toplevel.send_pending_configure();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.relayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clamp_pointer_position(&self, requested: Point<f64, Logical>) -> Point<f64, Logical> {
|
fn clamp_pointer_position(&self, requested: Point<f64, Logical>) -> Point<f64, Logical> {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#![allow(irrefutable_let_patterns)]
|
#![allow(irrefutable_let_patterns)]
|
||||||
|
|
||||||
mod backends;
|
mod backends;
|
||||||
|
mod config;
|
||||||
mod cursor;
|
mod cursor;
|
||||||
mod grabs;
|
mod grabs;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
@@ -15,15 +16,22 @@ pub use state::Ferese;
|
|||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::backends::LaunchConfig;
|
use crate::backends::LaunchConfig;
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
init_logging();
|
init_logging();
|
||||||
|
|
||||||
let launch = LaunchConfig::from_environment()
|
let launch = LaunchConfig::from_environment()
|
||||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
|
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
|
||||||
|
let config = Config::load()?;
|
||||||
let mut event_loop = EventLoop::try_new()?;
|
let mut event_loop = EventLoop::try_new()?;
|
||||||
let display = Display::new()?;
|
let display = Display::new()?;
|
||||||
let mut state = Ferese::new(&mut event_loop, display)?;
|
let mut state = Ferese::new(
|
||||||
|
&mut event_loop,
|
||||||
|
display,
|
||||||
|
config.layout_mode(),
|
||||||
|
config.default_column_width()?,
|
||||||
|
)?;
|
||||||
backends::init(launch.backend, &mut event_loop, &mut state)?;
|
backends::init(launch.backend, &mut event_loop, &mut state)?;
|
||||||
|
|
||||||
// SAFETY: the backend has already read the host display and Ferese is
|
// SAFETY: the backend has already read the host display and Ferese is
|
||||||
|
|||||||
+57
-28
@@ -7,8 +7,12 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use ferese_animation::{ClientSize, SpringConfig, WindowGeometry};
|
use ferese_animation::{ClientSize, SpringConfig, WindowGeometry};
|
||||||
use ferese_core::{OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceSet};
|
use ferese_core::{
|
||||||
use ferese_layout::{Axis, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, WindowId};
|
LayoutMode, OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceSet,
|
||||||
|
};
|
||||||
|
use ferese_layout::{
|
||||||
|
Axis, ColumnWidth, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, WindowId,
|
||||||
|
};
|
||||||
|
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::{
|
backend::{
|
||||||
@@ -267,6 +271,8 @@ impl Ferese {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
event_loop: &mut EventLoop<'static, Self>,
|
event_loop: &mut EventLoop<'static, Self>,
|
||||||
display: Display<Self>,
|
display: Display<Self>,
|
||||||
|
default_layout_mode: LayoutMode,
|
||||||
|
default_column_width: ColumnWidth,
|
||||||
) -> Result<Self, Box<dyn Error>> {
|
) -> Result<Self, Box<dyn Error>> {
|
||||||
let display_handle = display.handle();
|
let display_handle = display.handle();
|
||||||
crate::handlers::screencopy::init_global(&display_handle);
|
crate::handlers::screencopy::init_global(&display_handle);
|
||||||
@@ -315,7 +321,7 @@ impl Ferese {
|
|||||||
display_handle,
|
display_handle,
|
||||||
loop_signal: event_loop.get_signal(),
|
loop_signal: event_loop.get_signal(),
|
||||||
space: Space::default(),
|
space: Space::default(),
|
||||||
workspaces: WorkspaceSet::default(),
|
workspaces: WorkspaceSet::new(default_layout_mode, default_column_width),
|
||||||
output_workspaces: OutputWorkspaceMap::default(),
|
output_workspaces: OutputWorkspaceMap::default(),
|
||||||
output_ids: HashMap::new(),
|
output_ids: HashMap::new(),
|
||||||
output_identity_ids: HashMap::new(),
|
output_identity_ids: HashMap::new(),
|
||||||
@@ -620,6 +626,7 @@ impl Ferese {
|
|||||||
pub fn relayout(&mut self) {
|
pub fn relayout(&mut self) {
|
||||||
self.arrange_layers();
|
self.arrange_layers();
|
||||||
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
|
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
|
||||||
|
let constraints = self.window_constraints();
|
||||||
let mut visible = HashSet::new();
|
let mut visible = HashSet::new();
|
||||||
let mut placements = Vec::new();
|
let mut placements = Vec::new();
|
||||||
|
|
||||||
@@ -630,23 +637,28 @@ impl Ferese {
|
|||||||
let Some(workspace_id) = self.output_workspaces.active_workspace(output_id) else {
|
let Some(workspace_id) = self.output_workspaces.active_workspace(output_id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Some(workspace) = self.workspaces.workspace(workspace_id) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Some(bounds) = self.output_bounds_for(&output) else {
|
let Some(bounds) = self.output_bounds_for(&output) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let fullscreen_bounds = self.full_output_bounds_for(&output).unwrap_or(bounds);
|
let fullscreen_bounds = self.full_output_bounds_for(&output).unwrap_or(bounds);
|
||||||
|
let Some(workspace) = self.workspaces.workspace(workspace_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let workspace_fullscreen = workspace.fullscreen;
|
||||||
|
let workspace_focus = workspace.last_focused;
|
||||||
let focused = self
|
let focused = self
|
||||||
.focused_window
|
.focused_window
|
||||||
.filter(|window| {
|
.filter(|window| {
|
||||||
self.workspaces.workspace_for_window(*window) == Some(workspace_id)
|
self.workspaces.workspace_for_window(*window) == Some(workspace_id)
|
||||||
})
|
})
|
||||||
.or(workspace.last_focused);
|
.or(workspace_focus);
|
||||||
|
let Some(workspace) = self.workspaces.workspace_mut(workspace_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let layout = match workspace.layout.geometry_with_constraints(
|
let layout = match workspace.layout.geometry_with_constraints(
|
||||||
bounds,
|
bounds,
|
||||||
GapConfig::default(),
|
GapConfig::default(),
|
||||||
&self.window_constraints(),
|
&constraints,
|
||||||
focused,
|
focused,
|
||||||
) {
|
) {
|
||||||
Ok(layout) => layout,
|
Ok(layout) => layout,
|
||||||
@@ -671,9 +683,9 @@ impl Ferese {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rect = if workspace.fullscreen == Some(*id) {
|
let rect = if workspace_fullscreen == Some(*id) {
|
||||||
fullscreen_bounds
|
fullscreen_bounds
|
||||||
} else if workspace.fullscreen.is_some() {
|
} else if workspace_fullscreen.is_some() {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
match self.workspaces.placement(*id) {
|
match self.workspaces.placement(*id) {
|
||||||
@@ -687,7 +699,7 @@ impl Ferese {
|
|||||||
None => continue,
|
None => continue,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let is_fullscreen = workspace.fullscreen == Some(*id);
|
let is_fullscreen = workspace_fullscreen == Some(*id);
|
||||||
let is_floating = matches!(
|
let is_floating = matches!(
|
||||||
self.workspaces.placement(*id),
|
self.workspaces.placement(*id),
|
||||||
Some(WindowPlacement::Floating { .. })
|
Some(WindowPlacement::Floating { .. })
|
||||||
@@ -885,6 +897,8 @@ impl Ferese {
|
|||||||
toplevel.send_pending_configure();
|
toplevel.send_pending_configure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.relayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_direction(&mut self, direction: Direction) {
|
pub fn move_direction(&mut self, direction: Direction) {
|
||||||
@@ -926,6 +940,22 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn toggle_layout_mode(&mut self) {
|
||||||
|
let Some(bounds) = self.output_bounds() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mode = match self.workspaces.active().layout.mode() {
|
||||||
|
LayoutMode::Scrolling => LayoutMode::Tree,
|
||||||
|
LayoutMode::Tree => LayoutMode::Scrolling,
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.workspaces.set_active_layout_mode(mode, bounds) {
|
||||||
|
Ok(true) => self.relayout(),
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(error) => tracing::error!(%error, ?mode, "failed to change layout mode"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn toggle_focused_floating(&mut self) {
|
pub fn toggle_focused_floating(&mut self) {
|
||||||
let Some(window) = self.focused_window else {
|
let Some(window) = self.focused_window else {
|
||||||
return;
|
return;
|
||||||
@@ -1142,23 +1172,22 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn logical_window_rect(&self, window: WindowId, bounds: Rect) -> Option<Rect> {
|
fn logical_window_rect(&mut self, window: WindowId, bounds: Rect) -> Option<Rect> {
|
||||||
let workspace = self.workspaces.workspace_for_window(window)?;
|
let workspace = self.workspaces.workspace_for_window(window)?;
|
||||||
let workspace = self.workspaces.workspace(workspace)?;
|
let fullscreen = self.workspaces.workspace(workspace)?.fullscreen;
|
||||||
|
|
||||||
if workspace.fullscreen == Some(window) {
|
if fullscreen == Some(window) {
|
||||||
return Some(bounds);
|
return Some(bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let constraints = self.window_constraints();
|
||||||
|
let focused = self.focused_window;
|
||||||
match self.workspaces.placement(window)? {
|
match self.workspaces.placement(window)? {
|
||||||
WindowPlacement::Tiled => workspace
|
WindowPlacement::Tiled => self
|
||||||
|
.workspaces
|
||||||
|
.workspace_mut(workspace)?
|
||||||
.layout
|
.layout
|
||||||
.geometry_with_constraints(
|
.geometry_with_constraints(bounds, GapConfig::default(), &constraints, focused)
|
||||||
bounds,
|
|
||||||
GapConfig::default(),
|
|
||||||
&self.window_constraints(),
|
|
||||||
self.focused_window,
|
|
||||||
)
|
|
||||||
.ok()?
|
.ok()?
|
||||||
.geometry
|
.geometry
|
||||||
.get(&window)
|
.get(&window)
|
||||||
@@ -1167,13 +1196,13 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tiled_layout(&self, bounds: Rect) -> Result<LayoutResult, ferese_layout::LayoutError> {
|
fn tiled_layout(&mut self, bounds: Rect) -> Result<LayoutResult, ferese_layout::LayoutError> {
|
||||||
self.workspaces.active().layout.geometry_with_constraints(
|
let constraints = self.window_constraints();
|
||||||
bounds,
|
let focused = self.focused_window;
|
||||||
GapConfig::default(),
|
self.workspaces
|
||||||
&self.window_constraints(),
|
.active_mut()
|
||||||
self.focused_window,
|
.layout
|
||||||
)
|
.geometry_with_constraints(bounds, GapConfig::default(), &constraints, focused)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
|
fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
|
||||||
|
|||||||
@@ -2,4 +2,7 @@ mod output;
|
|||||||
mod workspace;
|
mod workspace;
|
||||||
|
|
||||||
pub use output::{OutputError, OutputGeometry, OutputId, OutputWorkspaceMap, WorkspaceSwitch};
|
pub use output::{OutputError, OutputGeometry, OutputId, OutputWorkspaceMap, WorkspaceSwitch};
|
||||||
pub use workspace::{WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceSet};
|
pub use workspace::{
|
||||||
|
LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout,
|
||||||
|
WorkspaceSet,
|
||||||
|
};
|
||||||
@@ -2,7 +2,10 @@ use std::collections::{HashMap, HashSet};
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use ferese_layout::{Axis, LayoutError, LayoutTree, Rect, WindowId};
|
use ferese_layout::{
|
||||||
|
Axis, ColumnWidth, Direction, GapConfig, LayoutError, LayoutResult, LayoutTree, Rect,
|
||||||
|
ScrollingLayout, SizeConstraints, WindowId,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
pub struct WorkspaceId(pub u64);
|
pub struct WorkspaceId(pub u64);
|
||||||
@@ -11,12 +14,237 @@ pub struct WorkspaceId(pub u64);
|
|||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
pub id: WorkspaceId,
|
pub id: WorkspaceId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub layout: LayoutTree,
|
pub layout: WorkspaceLayout,
|
||||||
pub floating: Vec<WindowId>,
|
pub floating: Vec<WindowId>,
|
||||||
pub last_focused: Option<WindowId>,
|
pub last_focused: Option<WindowId>,
|
||||||
pub fullscreen: Option<WindowId>,
|
pub fullscreen: Option<WindowId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub enum LayoutMode {
|
||||||
|
#[default]
|
||||||
|
Scrolling,
|
||||||
|
Tree,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum WorkspaceLayout {
|
||||||
|
Scrolling(ScrollingLayout),
|
||||||
|
Tree(LayoutTree),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WorkspaceLayout {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Scrolling(ScrollingLayout::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkspaceLayout {
|
||||||
|
pub fn new(mode: LayoutMode, default_column_width: ColumnWidth) -> Self {
|
||||||
|
match mode {
|
||||||
|
LayoutMode::Scrolling => {
|
||||||
|
Self::Scrolling(ScrollingLayout::with_default_width(default_column_width))
|
||||||
|
}
|
||||||
|
LayoutMode::Tree => Self::Tree(LayoutTree::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mode(&self) -> LayoutMode {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(_) => LayoutMode::Scrolling,
|
||||||
|
Self::Tree(_) => LayoutMode::Tree,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&self, window: WindowId) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(layout) => layout.contains(window),
|
||||||
|
Self::Tree(layout) => layout.contains(window),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window_ids(&self) -> Box<dyn Iterator<Item = WindowId> + '_> {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(layout) => Box::new(layout.window_ids()),
|
||||||
|
Self::Tree(layout) => Box::new(layout.window_ids()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
focused: Option<WindowId>,
|
||||||
|
axis: Axis,
|
||||||
|
ratio: f64,
|
||||||
|
) -> Result<(), LayoutError> {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(layout) => layout.insert(window, focused),
|
||||||
|
Self::Tree(layout) => layout.insert(window, focused, axis, ratio).map(drop),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(layout) => layout.remove(window),
|
||||||
|
Self::Tree(layout) => layout.remove(window),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stack_window(&mut self, window: WindowId, target: WindowId) -> Result<(), LayoutError> {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(layout) => layout.move_into_column(window, target),
|
||||||
|
Self::Tree(layout) => layout.stack_window(window, target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_window(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(layout) => layout.focus(window),
|
||||||
|
Self::Tree(layout) => layout.activate_window(window),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn geometry(&self, bounds: Rect) -> Result<HashMap<WindowId, Rect>, LayoutError> {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(layout) => {
|
||||||
|
let mut layout = layout.clone();
|
||||||
|
layout
|
||||||
|
.geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), None)
|
||||||
|
.map(|result| result.geometry)
|
||||||
|
}
|
||||||
|
Self::Tree(layout) => layout.geometry(bounds),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn geometry_with_constraints(
|
||||||
|
&mut self,
|
||||||
|
bounds: Rect,
|
||||||
|
gaps: GapConfig,
|
||||||
|
constraints: &HashMap<WindowId, SizeConstraints>,
|
||||||
|
focused: Option<WindowId>,
|
||||||
|
) -> Result<LayoutResult, LayoutError> {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(layout) => layout.geometry_with_constraints(
|
||||||
|
bounds,
|
||||||
|
gaps,
|
||||||
|
constraints,
|
||||||
|
focused.filter(|window| layout.contains(*window)),
|
||||||
|
),
|
||||||
|
Self::Tree(layout) => {
|
||||||
|
layout.geometry_with_constraints(bounds, gaps, constraints, focused)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn automatic_axis(
|
||||||
|
&self,
|
||||||
|
focused: Option<WindowId>,
|
||||||
|
bounds: Rect,
|
||||||
|
) -> Result<Axis, LayoutError> {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(_) => Ok(Axis::Horizontal),
|
||||||
|
Self::Tree(layout) => layout.automatic_axis(focused, bounds),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn directional_neighbor(
|
||||||
|
&self,
|
||||||
|
window: WindowId,
|
||||||
|
direction: Direction,
|
||||||
|
bounds: Rect,
|
||||||
|
) -> Result<Option<WindowId>, LayoutError> {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(layout) => layout.directional_neighbor(window, direction),
|
||||||
|
Self::Tree(layout) => layout.directional_neighbor(window, direction, bounds),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_window(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
direction: Direction,
|
||||||
|
bounds: Rect,
|
||||||
|
) -> Result<bool, LayoutError> {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(layout) => layout.move_window(window, direction),
|
||||||
|
Self::Tree(layout) => layout.move_window(window, direction, bounds),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize_window(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
direction: Direction,
|
||||||
|
amount: f64,
|
||||||
|
) -> Result<bool, LayoutError> {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(layout) => layout.resize_window(window, direction, amount),
|
||||||
|
Self::Tree(layout) => layout.resize_window(window, direction, amount),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self) -> Result<(), LayoutError> {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(layout) => layout.validate(),
|
||||||
|
Self::Tree(layout) => layout.validate(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_mode(
|
||||||
|
&mut self,
|
||||||
|
mode: LayoutMode,
|
||||||
|
bounds: Rect,
|
||||||
|
focused: Option<WindowId>,
|
||||||
|
default_column_width: ColumnWidth,
|
||||||
|
) -> Result<bool, LayoutError> {
|
||||||
|
if self.mode() == mode {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let replacement = match self {
|
||||||
|
Self::Scrolling(layout) => {
|
||||||
|
let columns = layout.columns().to_vec();
|
||||||
|
let mut tree = LayoutTree::default();
|
||||||
|
let mut previous = None;
|
||||||
|
|
||||||
|
for column in columns {
|
||||||
|
for (row, window) in column.windows.into_iter().enumerate() {
|
||||||
|
let axis = if row == 0 {
|
||||||
|
Axis::Horizontal
|
||||||
|
} else {
|
||||||
|
Axis::Vertical
|
||||||
|
};
|
||||||
|
tree.insert(window, previous, axis, 0.5)?;
|
||||||
|
previous = Some(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(focused) = focused.filter(|window| tree.contains(*window)) {
|
||||||
|
tree.activate_window(focused)?;
|
||||||
|
}
|
||||||
|
Self::Tree(tree)
|
||||||
|
}
|
||||||
|
Self::Tree(layout) => {
|
||||||
|
let windows = layout.window_ids_in_reading_order(bounds)?;
|
||||||
|
let mut scrolling = ScrollingLayout::with_default_width(default_column_width);
|
||||||
|
let mut previous = None;
|
||||||
|
|
||||||
|
for window in windows {
|
||||||
|
scrolling.insert(window, previous)?;
|
||||||
|
previous = Some(window);
|
||||||
|
}
|
||||||
|
if let Some(focused) = focused.filter(|window| scrolling.contains(*window)) {
|
||||||
|
scrolling.focus(focused)?;
|
||||||
|
}
|
||||||
|
Self::Scrolling(scrolling)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
*self = replacement;
|
||||||
|
debug_assert!(self.validate().is_ok());
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum WindowPlacement {
|
pub enum WindowPlacement {
|
||||||
Tiled,
|
Tiled,
|
||||||
@@ -70,16 +298,24 @@ pub struct WorkspaceSet {
|
|||||||
names: HashMap<String, WorkspaceId>,
|
names: HashMap<String, WorkspaceId>,
|
||||||
window_workspaces: HashMap<WindowId, WorkspaceId>,
|
window_workspaces: HashMap<WindowId, WorkspaceId>,
|
||||||
placements: HashMap<WindowId, WindowPlacement>,
|
placements: HashMap<WindowId, WindowPlacement>,
|
||||||
|
default_layout_mode: LayoutMode,
|
||||||
|
default_column_width: ColumnWidth,
|
||||||
next_id: u64,
|
next_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for WorkspaceSet {
|
impl Default for WorkspaceSet {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
Self::new(LayoutMode::default(), ColumnWidth::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkspaceSet {
|
||||||
|
pub fn new(default_layout_mode: LayoutMode, default_column_width: ColumnWidth) -> Self {
|
||||||
let active = WorkspaceId(1);
|
let active = WorkspaceId(1);
|
||||||
let workspace = Workspace {
|
let workspace = Workspace {
|
||||||
id: active,
|
id: active,
|
||||||
name: "1".to_owned(),
|
name: "1".to_owned(),
|
||||||
layout: LayoutTree::default(),
|
layout: WorkspaceLayout::new(default_layout_mode, default_column_width),
|
||||||
floating: Vec::new(),
|
floating: Vec::new(),
|
||||||
last_focused: None,
|
last_focused: None,
|
||||||
fullscreen: None,
|
fullscreen: None,
|
||||||
@@ -93,12 +329,12 @@ impl Default for WorkspaceSet {
|
|||||||
names,
|
names,
|
||||||
window_workspaces: HashMap::new(),
|
window_workspaces: HashMap::new(),
|
||||||
placements: HashMap::new(),
|
placements: HashMap::new(),
|
||||||
|
default_layout_mode,
|
||||||
|
default_column_width,
|
||||||
next_id: 2,
|
next_id: 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl WorkspaceSet {
|
|
||||||
pub fn active_id(&self) -> WorkspaceId {
|
pub fn active_id(&self) -> WorkspaceId {
|
||||||
self.active
|
self.active
|
||||||
}
|
}
|
||||||
@@ -130,6 +366,10 @@ impl WorkspaceSet {
|
|||||||
self.workspaces.get(&id)
|
self.workspaces.get(&id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn workspace_mut(&mut self, id: WorkspaceId) -> Option<&mut Workspace> {
|
||||||
|
self.workspaces.get_mut(&id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn workspace_for_window(&self, window: WindowId) -> Option<WorkspaceId> {
|
pub fn workspace_for_window(&self, window: WindowId) -> Option<WorkspaceId> {
|
||||||
self.window_workspaces.get(&window).copied()
|
self.window_workspaces.get(&window).copied()
|
||||||
}
|
}
|
||||||
@@ -138,6 +378,22 @@ impl WorkspaceSet {
|
|||||||
self.placements.get(&window).copied()
|
self.placements.get(&window).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_active_layout_mode(
|
||||||
|
&mut self,
|
||||||
|
mode: LayoutMode,
|
||||||
|
bounds: Rect,
|
||||||
|
) -> Result<bool, WorkspaceError> {
|
||||||
|
let default_column_width = self.default_column_width;
|
||||||
|
let workspace = self.active_mut();
|
||||||
|
let focused = tiled_focus(workspace);
|
||||||
|
let changed = workspace
|
||||||
|
.layout
|
||||||
|
.set_mode(mode, bounds, focused, default_column_width)?;
|
||||||
|
|
||||||
|
debug_assert!(self.validate().is_ok());
|
||||||
|
Ok(changed)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_floating_rect(
|
pub fn set_floating_rect(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: WindowId,
|
window: WindowId,
|
||||||
@@ -235,7 +491,7 @@ impl WorkspaceSet {
|
|||||||
Workspace {
|
Workspace {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
layout: LayoutTree::default(),
|
layout: WorkspaceLayout::new(self.default_layout_mode, self.default_column_width),
|
||||||
floating: Vec::new(),
|
floating: Vec::new(),
|
||||||
last_focused: None,
|
last_focused: None,
|
||||||
fullscreen: None,
|
fullscreen: None,
|
||||||
@@ -604,6 +860,28 @@ mod tests {
|
|||||||
assert!(workspaces.validate().is_ok());
|
assert!(workspaces.validate().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn configured_layout_defaults_apply_to_new_workspaces() {
|
||||||
|
let mut workspaces = WorkspaceSet::new(LayoutMode::Scrolling, ColumnWidth::Full);
|
||||||
|
workspaces
|
||||||
|
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
let first = workspaces
|
||||||
|
.active_mut()
|
||||||
|
.layout
|
||||||
|
.geometry_with_constraints(
|
||||||
|
Rect::new(0.0, 0.0, 1_000.0, 800.0),
|
||||||
|
GapConfig::default(),
|
||||||
|
&HashMap::new(),
|
||||||
|
Some(WindowId(1)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(first.geometry[&WindowId(1)].width, 1_000.0);
|
||||||
|
workspaces.switch_to_numeric(2).unwrap();
|
||||||
|
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn switching_restores_workspace_focus() {
|
fn switching_restores_workspace_focus() {
|
||||||
let mut workspaces = WorkspaceSet::default();
|
let mut workspaces = WorkspaceSet::default();
|
||||||
@@ -840,7 +1118,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stacking_tiled_windows_preserves_workspace_membership() {
|
fn grouping_tiled_windows_preserves_workspace_membership() {
|
||||||
let mut workspaces = WorkspaceSet::default();
|
let mut workspaces = WorkspaceSet::default();
|
||||||
workspaces
|
workspaces
|
||||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||||
@@ -851,6 +1129,7 @@ mod tests {
|
|||||||
workspaces.stack_window(WindowId(2), WindowId(1)).unwrap();
|
workspaces.stack_window(WindowId(2), WindowId(1)).unwrap();
|
||||||
|
|
||||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
||||||
|
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
workspaces
|
workspaces
|
||||||
.active()
|
.active()
|
||||||
@@ -858,11 +1137,48 @@ mod tests {
|
|||||||
.geometry(Rect::new(0.0, 0.0, 100.0, 80.0))
|
.geometry(Rect::new(0.0, 0.0, 100.0, 80.0))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.len(),
|
.len(),
|
||||||
1
|
2
|
||||||
);
|
);
|
||||||
assert!(workspaces.validate().is_ok());
|
assert!(workspaces.validate().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layout_mode_conversion_preserves_membership_and_focus() {
|
||||||
|
let mut workspaces = WorkspaceSet::default();
|
||||||
|
for id in 1..=4 {
|
||||||
|
workspaces
|
||||||
|
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
workspaces.focus_window(WindowId(2)).unwrap();
|
||||||
|
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
workspaces
|
||||||
|
.set_active_layout_mode(LayoutMode::Tree, bounds)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Tree);
|
||||||
|
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
||||||
|
assert_eq!(
|
||||||
|
workspaces
|
||||||
|
.active()
|
||||||
|
.layout
|
||||||
|
.window_ids()
|
||||||
|
.collect::<HashSet<_>>(),
|
||||||
|
HashSet::from([WindowId(1), WindowId(2), WindowId(3), WindowId(4)])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
workspaces
|
||||||
|
.set_active_layout_mode(LayoutMode::Scrolling, bounds)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling);
|
||||||
|
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
||||||
|
assert!(workspaces.validate().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn randomized_workspace_sequences_preserve_all_invariants() {
|
fn randomized_workspace_sequences_preserve_all_invariants() {
|
||||||
let mut workspaces = WorkspaceSet::default();
|
let mut workspaces = WorkspaceSet::default();
|
||||||
|
|||||||
@@ -170,6 +170,18 @@ impl LayoutTree {
|
|||||||
self.windows.keys().copied()
|
self.windows.keys().copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn window_ids_in_reading_order(&self, bounds: Rect) -> Result<Vec<WindowId>, LayoutError> {
|
||||||
|
let geometry = self.geometry(bounds)?;
|
||||||
|
let mut windows = geometry.into_iter().collect::<Vec<_>>();
|
||||||
|
windows.sort_by(|(left_window, left), (right_window, right)| {
|
||||||
|
left.x
|
||||||
|
.total_cmp(&right.x)
|
||||||
|
.then_with(|| left.y.total_cmp(&right.y))
|
||||||
|
.then_with(|| left_window.0.cmp(&right_window.0))
|
||||||
|
});
|
||||||
|
Ok(windows.into_iter().map(|(window, _)| window).collect())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert(
|
pub fn insert(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: WindowId,
|
window: WindowId,
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ConstraintKind, ConstraintWarning, GapConfig, LayoutError, LayoutResult, Rect, SizeConstraints,
|
ConstraintKind, ConstraintWarning, Direction, GapConfig, LayoutError, LayoutResult, Rect,
|
||||||
WindowId, normalized_constraints, record_minimum_warnings,
|
SizeConstraints, WindowId, normalized_constraints, record_minimum_warnings,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_WIDTH: f64 = 0.5;
|
const DEFAULT_WIDTH: f64 = 0.5;
|
||||||
|
const MIN_COLUMN_WIDTH: f64 = 0.1;
|
||||||
|
const MAX_COLUMN_WIDTH: f64 = 2.0;
|
||||||
|
const MIN_ROW_HEIGHT: f64 = 0.05;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum ColumnWidth {
|
pub enum ColumnWidth {
|
||||||
@@ -29,11 +32,11 @@ pub struct Column {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Column {
|
impl Column {
|
||||||
fn new(window: WindowId) -> Self {
|
fn new(window: WindowId, width: ColumnWidth) -> Self {
|
||||||
Self {
|
Self {
|
||||||
windows: vec![window],
|
windows: vec![window],
|
||||||
active: 0,
|
active: 0,
|
||||||
width: ColumnWidth::default(),
|
width: normalized_width(width),
|
||||||
heights: vec![1.0],
|
heights: vec![1.0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,6 +66,7 @@ pub struct ScrollingLayout {
|
|||||||
active_column: Option<usize>,
|
active_column: Option<usize>,
|
||||||
viewport_x: f64,
|
viewport_x: f64,
|
||||||
neighbor_context: f64,
|
neighbor_context: f64,
|
||||||
|
default_width: ColumnWidth,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ScrollingLayout {
|
impl Default for ScrollingLayout {
|
||||||
@@ -72,11 +76,23 @@ impl Default for ScrollingLayout {
|
|||||||
active_column: None,
|
active_column: None,
|
||||||
viewport_x: 0.0,
|
viewport_x: 0.0,
|
||||||
neighbor_context: 48.0,
|
neighbor_context: 48.0,
|
||||||
|
default_width: ColumnWidth::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScrollingLayout {
|
impl ScrollingLayout {
|
||||||
|
pub fn with_default_width(default_width: ColumnWidth) -> Self {
|
||||||
|
Self {
|
||||||
|
default_width: normalized_width(default_width),
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_width(&self) -> ColumnWidth {
|
||||||
|
self.default_width
|
||||||
|
}
|
||||||
|
|
||||||
pub fn columns(&self) -> &[Column] {
|
pub fn columns(&self) -> &[Column] {
|
||||||
&self.columns
|
&self.columns
|
||||||
}
|
}
|
||||||
@@ -112,7 +128,8 @@ impl ScrollingLayout {
|
|||||||
.and_then(|focused| self.window_location(focused))
|
.and_then(|focused| self.window_location(focused))
|
||||||
.map(|(column, _)| column + 1)
|
.map(|(column, _)| column + 1)
|
||||||
.unwrap_or(self.columns.len());
|
.unwrap_or(self.columns.len());
|
||||||
self.columns.insert(index, Column::new(window));
|
self.columns
|
||||||
|
.insert(index, Column::new(window, self.default_width));
|
||||||
self.active_column = Some(index);
|
self.active_column = Some(index);
|
||||||
|
|
||||||
debug_assert!(self.validate().is_ok());
|
debug_assert!(self.validate().is_ok());
|
||||||
@@ -190,7 +207,8 @@ impl ScrollingLayout {
|
|||||||
|
|
||||||
self.remove(window)?;
|
self.remove(window)?;
|
||||||
let insertion = (source + 1).min(self.columns.len());
|
let insertion = (source + 1).min(self.columns.len());
|
||||||
self.columns.insert(insertion, Column::new(window));
|
self.columns
|
||||||
|
.insert(insertion, Column::new(window, self.default_width));
|
||||||
self.active_column = Some(insertion);
|
self.active_column = Some(insertion);
|
||||||
|
|
||||||
debug_assert!(self.validate().is_ok());
|
debug_assert!(self.validate().is_ok());
|
||||||
@@ -209,6 +227,109 @@ impl ScrollingLayout {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn directional_neighbor(
|
||||||
|
&self,
|
||||||
|
window: WindowId,
|
||||||
|
direction: Direction,
|
||||||
|
) -> Result<Option<WindowId>, LayoutError> {
|
||||||
|
let (column, row) = self
|
||||||
|
.window_location(window)
|
||||||
|
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||||
|
let neighbor = match direction {
|
||||||
|
Direction::Up => row
|
||||||
|
.checked_sub(1)
|
||||||
|
.map(|row| self.columns[column].windows[row]),
|
||||||
|
Direction::Down => self.columns[column].windows.get(row + 1).copied(),
|
||||||
|
Direction::Left => column.checked_sub(1).map(|column| {
|
||||||
|
let column = &self.columns[column];
|
||||||
|
column.windows[column.active]
|
||||||
|
}),
|
||||||
|
Direction::Right => self
|
||||||
|
.columns
|
||||||
|
.get(column + 1)
|
||||||
|
.map(|column| column.windows[column.active]),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(neighbor)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_window(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
direction: Direction,
|
||||||
|
) -> Result<bool, LayoutError> {
|
||||||
|
let (column, row) = self
|
||||||
|
.window_location(window)
|
||||||
|
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||||
|
|
||||||
|
match direction {
|
||||||
|
Direction::Up if row > 0 => {
|
||||||
|
self.columns[column].windows.swap(row, row - 1);
|
||||||
|
self.columns[column].heights.swap(row, row - 1);
|
||||||
|
self.columns[column].active = row - 1;
|
||||||
|
}
|
||||||
|
Direction::Down if row + 1 < self.columns[column].windows.len() => {
|
||||||
|
self.columns[column].windows.swap(row, row + 1);
|
||||||
|
self.columns[column].heights.swap(row, row + 1);
|
||||||
|
self.columns[column].active = row + 1;
|
||||||
|
}
|
||||||
|
Direction::Left if column > 0 => self.move_horizontally(window, column - 1)?,
|
||||||
|
Direction::Right if column + 1 < self.columns.len() => {
|
||||||
|
self.move_horizontally(window, column + 1)?
|
||||||
|
}
|
||||||
|
_ => return Ok(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
debug_assert!(self.validate().is_ok());
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize_window(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
direction: Direction,
|
||||||
|
amount: f64,
|
||||||
|
) -> Result<bool, LayoutError> {
|
||||||
|
let (column, row) = self
|
||||||
|
.window_location(window)
|
||||||
|
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||||
|
let amount = if amount.is_finite() {
|
||||||
|
amount.abs()
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
if amount == 0.0 {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let changed = match direction {
|
||||||
|
Direction::Left | Direction::Right => {
|
||||||
|
let current = match normalized_width(self.columns[column].width) {
|
||||||
|
ColumnWidth::Proportion(value) => value,
|
||||||
|
ColumnWidth::Fixed(_) | ColumnWidth::Full => DEFAULT_WIDTH,
|
||||||
|
};
|
||||||
|
let adjustment = if direction == Direction::Right {
|
||||||
|
amount
|
||||||
|
} else {
|
||||||
|
-amount
|
||||||
|
};
|
||||||
|
let resized = (current + adjustment).clamp(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
|
||||||
|
self.columns[column].width = ColumnWidth::Proportion(resized);
|
||||||
|
resized != current
|
||||||
|
}
|
||||||
|
Direction::Up if row > 0 => {
|
||||||
|
resize_rows(&mut self.columns[column], row, row - 1, amount)
|
||||||
|
}
|
||||||
|
Direction::Down if row + 1 < self.columns[column].windows.len() => {
|
||||||
|
resize_rows(&mut self.columns[column], row, row + 1, amount)
|
||||||
|
}
|
||||||
|
Direction::Up | Direction::Down => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
debug_assert!(self.validate().is_ok());
|
||||||
|
Ok(changed)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn geometry_with_constraints(
|
pub fn geometry_with_constraints(
|
||||||
&mut self,
|
&mut self,
|
||||||
bounds: Rect,
|
bounds: Rect,
|
||||||
@@ -314,6 +435,33 @@ impl ScrollingLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn move_horizontally(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
destination: usize,
|
||||||
|
) -> Result<(), LayoutError> {
|
||||||
|
let (source, _) = self
|
||||||
|
.window_location(window)
|
||||||
|
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||||
|
if self.columns[source].windows.len() == 1 {
|
||||||
|
self.columns.swap(source, destination);
|
||||||
|
self.active_column = Some(destination);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let insertion = if destination < source {
|
||||||
|
source
|
||||||
|
} else {
|
||||||
|
source + 1
|
||||||
|
};
|
||||||
|
self.remove(window)?;
|
||||||
|
let insertion = insertion.min(self.columns.len());
|
||||||
|
self.columns
|
||||||
|
.insert(insertion, Column::new(window, self.default_width));
|
||||||
|
self.active_column = Some(insertion);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn window_location(&self, window: WindowId) -> Option<(usize, usize)> {
|
fn window_location(&self, window: WindowId) -> Option<(usize, usize)> {
|
||||||
self.columns.iter().enumerate().find_map(|(column, data)| {
|
self.columns.iter().enumerate().find_map(|(column, data)| {
|
||||||
data.windows
|
data.windows
|
||||||
@@ -323,7 +471,7 @@ impl ScrollingLayout {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self) -> Result<(), LayoutError> {
|
pub fn validate(&self) -> Result<(), LayoutError> {
|
||||||
if self.columns.is_empty() {
|
if self.columns.is_empty() {
|
||||||
return if self.active_column.is_none() {
|
return if self.active_column.is_none() {
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -370,6 +518,18 @@ impl ScrollingLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resize_rows(column: &mut Column, row: usize, neighbor: usize, amount: f64) -> bool {
|
||||||
|
let transferable = (column.heights[neighbor] - MIN_ROW_HEIGHT).max(0.0);
|
||||||
|
let adjustment = amount.min(transferable);
|
||||||
|
if adjustment == 0.0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
column.heights[row] += adjustment;
|
||||||
|
column.heights[neighbor] -= adjustment;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn normalized_width(width: ColumnWidth) -> ColumnWidth {
|
fn normalized_width(width: ColumnWidth) -> ColumnWidth {
|
||||||
match width {
|
match width {
|
||||||
ColumnWidth::Proportion(value) if value.is_finite() && value > 0.0 => {
|
ColumnWidth::Proportion(value) if value.is_finite() && value > 0.0 => {
|
||||||
@@ -441,6 +601,21 @@ mod tests {
|
|||||||
assert_eq!(layout.active_column(), Some(1));
|
assert_eq!(layout.active_column(), Some(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn configured_default_width_is_applied_to_new_columns() {
|
||||||
|
let mut layout = ScrollingLayout::with_default_width(ColumnWidth::Full);
|
||||||
|
layout.insert(window(1), None).unwrap();
|
||||||
|
layout.insert(window(2), Some(window(1))).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(layout.default_width(), ColumnWidth::Full);
|
||||||
|
assert!(
|
||||||
|
layout
|
||||||
|
.columns()
|
||||||
|
.iter()
|
||||||
|
.all(|column| column.width == ColumnWidth::Full)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn focus_reveal_scrolls_only_as_far_as_needed() {
|
fn focus_reveal_scrolls_only_as_far_as_needed() {
|
||||||
let mut layout = ScrollingLayout::default();
|
let mut layout = ScrollingLayout::default();
|
||||||
@@ -505,4 +680,66 @@ mod tests {
|
|||||||
assert_eq!(first.x, second.x);
|
assert_eq!(first.x, second.x);
|
||||||
assert_eq!(first.width, second.width);
|
assert_eq!(first.width, second.width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn directional_navigation_follows_columns_and_rows() {
|
||||||
|
let mut layout = ScrollingLayout::default();
|
||||||
|
layout.insert(window(1), None).unwrap();
|
||||||
|
layout.insert(window(2), Some(window(1))).unwrap();
|
||||||
|
layout.insert(window(3), Some(window(2))).unwrap();
|
||||||
|
layout.move_into_column(window(3), window(2)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
layout.directional_neighbor(window(1), Direction::Right),
|
||||||
|
Ok(Some(window(3)))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
layout.directional_neighbor(window(3), Direction::Up),
|
||||||
|
Ok(Some(window(2)))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
layout.directional_neighbor(window(2), Direction::Down),
|
||||||
|
Ok(Some(window(3)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vertical_moves_reorder_rows_and_horizontal_moves_extract_them() {
|
||||||
|
let mut layout = ScrollingLayout::default();
|
||||||
|
layout.insert(window(1), None).unwrap();
|
||||||
|
layout.insert(window(2), Some(window(1))).unwrap();
|
||||||
|
layout.insert(window(3), Some(window(2))).unwrap();
|
||||||
|
layout.move_into_column(window(3), window(2)).unwrap();
|
||||||
|
|
||||||
|
assert!(layout.move_window(window(3), Direction::Up).unwrap());
|
||||||
|
assert_eq!(layout.columns()[1].windows, vec![window(3), window(2)]);
|
||||||
|
assert!(layout.move_window(window(3), Direction::Left).unwrap());
|
||||||
|
assert_eq!(layout.columns().len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
layout.window_ids().collect::<Vec<_>>(),
|
||||||
|
vec![window(1), window(3), window(2)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resize_changes_column_width_and_neighboring_row_weights() {
|
||||||
|
let mut layout = ScrollingLayout::default();
|
||||||
|
layout.insert(window(1), None).unwrap();
|
||||||
|
layout.insert(window(2), Some(window(1))).unwrap();
|
||||||
|
layout.move_into_column(window(2), window(1)).unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
layout
|
||||||
|
.resize_window(window(1), Direction::Right, 0.1)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(0.6));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
layout
|
||||||
|
.resize_window(window(1), Direction::Down, 0.1)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(layout.columns()[0].heights, vec![0.6, 0.4]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1024,6 +1024,7 @@ smart_gaps = true
|
|||||||
|
|
||||||
[scrolling]
|
[scrolling]
|
||||||
default_column_width = 0.5
|
default_column_width = 0.5
|
||||||
|
# Use 1.0 or "full" to open every new column at the viewport width.
|
||||||
width_presets = [0.333333, 0.5, 0.666667, 1.0]
|
width_presets = [0.333333, 0.5, 0.666667, 1.0]
|
||||||
neighbor_context = 48.0
|
neighbor_context = 48.0
|
||||||
new_window = "column_after_focused"
|
new_window = "column_after_focused"
|
||||||
@@ -1244,6 +1245,7 @@ Super+Shift+H/J/K/L move left/down/up/right
|
|||||||
Super+1..9 switch workspace
|
Super+1..9 switch workspace
|
||||||
Super+Shift+1..9 move window to workspace
|
Super+Shift+1..9 move window to workspace
|
||||||
Super+F toggle fullscreen
|
Super+F toggle fullscreen
|
||||||
|
Super+M toggle scrolling/tree layout
|
||||||
Super+Shift+Space toggle floating
|
Super+Shift+Space toggle floating
|
||||||
Super+Space launcher
|
Super+Space launcher
|
||||||
Super+Tab overview
|
Super+Tab overview
|
||||||
|
|||||||
Reference in new issue
Block a user