fix: preserve default outer gaps
This commit is contained in:
1 parent
ed529ae900
commit
94a83c83bb
7 files changed
+119
-14
No files matched your search
@@ -37,6 +37,14 @@ default_column_width = "full"
|
|||||||
`default_column_width = 1.0` is equivalent. `Super+M` switches the active
|
`default_column_width = 1.0` is equivalent. `Super+M` switches the active
|
||||||
workspace between scrolling and tree modes.
|
workspace between scrolling and tree modes.
|
||||||
|
|
||||||
|
Outer gaps remain visible with one window by default. To intentionally remove
|
||||||
|
them for a lone tiled window:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[layout]
|
||||||
|
smart_gaps = true
|
||||||
|
```
|
||||||
|
|
||||||
Scrolling keeps windows in stable workspace coordinates and animates the
|
Scrolling keeps windows in stable workspace coordinates and animates the
|
||||||
viewport only when focus must be revealed. Set
|
viewport only when focus must be revealed. Set
|
||||||
`focus_strategy = "center_on_focus"` under `[scrolling]` to center focused
|
`focus_strategy = "center_on_focus"` under `[scrolling]` to center focused
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use ferese_animation::SpringConfig;
|
use ferese_animation::SpringConfig;
|
||||||
use ferese_core::LayoutMode;
|
use ferese_core::LayoutMode;
|
||||||
use ferese_layout::{ColumnWidth, ViewportFocusStrategy};
|
use ferese_layout::{ColumnWidth, GapConfig, ViewportFocusStrategy};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
@@ -86,9 +86,26 @@ impl Default for SpringSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct LayoutConfig {
|
struct LayoutConfig {
|
||||||
mode: Option<LayoutModeValue>,
|
mode: Option<LayoutModeValue>,
|
||||||
|
#[serde(default = "default_inner_gap")]
|
||||||
|
inner_gap: f64,
|
||||||
|
#[serde(default = "default_outer_gap")]
|
||||||
|
outer_gap: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
smart_gaps: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LayoutConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mode: None,
|
||||||
|
inner_gap: default_inner_gap(),
|
||||||
|
outer_gap: default_outer_gap(),
|
||||||
|
smart_gaps: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||||
@@ -138,6 +155,10 @@ pub enum ConfigError {
|
|||||||
field: &'static str,
|
field: &'static str,
|
||||||
value: f64,
|
value: f64,
|
||||||
},
|
},
|
||||||
|
InvalidLayoutValue {
|
||||||
|
field: &'static str,
|
||||||
|
value: f64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ConfigError {
|
impl fmt::Display for ConfigError {
|
||||||
@@ -156,6 +177,9 @@ impl fmt::Display for ConfigError {
|
|||||||
Self::InvalidAnimationValue { field, value } => {
|
Self::InvalidAnimationValue { field, value } => {
|
||||||
write!(formatter, "invalid animations.{field} value {value}")
|
write!(formatter, "invalid animations.{field} value {value}")
|
||||||
}
|
}
|
||||||
|
Self::InvalidLayoutValue { field, value } => {
|
||||||
|
write!(formatter, "invalid layout.{field} value {value}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +189,9 @@ impl Error for ConfigError {
|
|||||||
match self {
|
match self {
|
||||||
Self::Read { source, .. } => Some(source),
|
Self::Read { source, .. } => Some(source),
|
||||||
Self::Parse { source, .. } => Some(source),
|
Self::Parse { source, .. } => Some(source),
|
||||||
Self::InvalidColumnWidth { .. } | Self::InvalidAnimationValue { .. } => None,
|
Self::InvalidColumnWidth { .. }
|
||||||
|
| Self::InvalidAnimationValue { .. }
|
||||||
|
| Self::InvalidLayoutValue { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,6 +217,17 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn gap_config(&self) -> Result<GapConfig, ConfigError> {
|
||||||
|
let inner = nonnegative_layout_value(self.layout.inner_gap, "inner_gap")?;
|
||||||
|
let outer = nonnegative_layout_value(self.layout.outer_gap, "outer_gap")?;
|
||||||
|
|
||||||
|
Ok(GapConfig {
|
||||||
|
inner,
|
||||||
|
outer,
|
||||||
|
smart: self.layout.smart_gaps,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default_column_width(&self) -> Result<ColumnWidth, ConfigError> {
|
pub fn default_column_width(&self) -> Result<ColumnWidth, ConfigError> {
|
||||||
parse_column_width(
|
parse_column_width(
|
||||||
self.scrolling
|
self.scrolling
|
||||||
@@ -286,10 +323,26 @@ fn positive_animation_value(value: f64, field: &'static str) -> Result<f64, Conf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn nonnegative_layout_value(value: f64, field: &'static str) -> Result<f64, ConfigError> {
|
||||||
|
if value.is_finite() && value >= 0.0 {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(ConfigError::InvalidLayoutValue { field, value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fn enabled_by_default() -> bool {
|
const fn enabled_by_default() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fn default_inner_gap() -> f64 {
|
||||||
|
10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_outer_gap() -> f64 {
|
||||||
|
10.0
|
||||||
|
}
|
||||||
|
|
||||||
const fn default_animation_speed() -> f64 {
|
const fn default_animation_speed() -> f64 {
|
||||||
1.0
|
1.0
|
||||||
}
|
}
|
||||||
@@ -367,6 +420,7 @@ mod tests {
|
|||||||
config.default_column_width().unwrap(),
|
config.default_column_width().unwrap(),
|
||||||
ColumnWidth::Proportion(0.5)
|
ColumnWidth::Proportion(0.5)
|
||||||
);
|
);
|
||||||
|
assert_eq!(config.gap_config().unwrap(), GapConfig::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -449,6 +503,22 @@ mod tests {
|
|||||||
assert!(damping.spring_config().is_err());
|
assert!(damping.spring_config().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_and_validates_layout_gaps() {
|
||||||
|
let configured = parse("[layout]\ninner_gap = 6.0\nouter_gap = 14.0\nsmart_gaps = true");
|
||||||
|
let invalid = parse("[layout]\nouter_gap = -1.0");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
configured.gap_config().unwrap(),
|
||||||
|
GapConfig {
|
||||||
|
inner: 6.0,
|
||||||
|
outer: 14.0,
|
||||||
|
smart: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert!(invalid.gap_config().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn derives_critical_viewport_damping() {
|
fn derives_critical_viewport_damping() {
|
||||||
let config = parse("");
|
let config = parse("");
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let display = Display::new()?;
|
let display = Display::new()?;
|
||||||
let runtime = RuntimeConfig {
|
let runtime = RuntimeConfig {
|
||||||
layout_mode: config.layout_mode(),
|
layout_mode: config.layout_mode(),
|
||||||
|
gap_config: config.gap_config()?,
|
||||||
default_column_width: config.default_column_width()?,
|
default_column_width: config.default_column_width()?,
|
||||||
scrolling_focus_strategy: config.scrolling_focus_strategy(),
|
scrolling_focus_strategy: config.scrolling_focus_strategy(),
|
||||||
column_width_presets: config.width_presets()?,
|
column_width_presets: config.width_presets()?,
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ pub struct Ferese {
|
|||||||
scrolling_world_x: HashMap<WindowId, (WorkspaceId, AnimatedValue)>,
|
scrolling_world_x: HashMap<WindowId, (WorkspaceId, AnimatedValue)>,
|
||||||
pub focused_window: Option<WindowId>,
|
pub focused_window: Option<WindowId>,
|
||||||
column_width_presets: Vec<ColumnWidth>,
|
column_width_presets: Vec<ColumnWidth>,
|
||||||
|
gap_config: GapConfig,
|
||||||
animations_enabled: bool,
|
animations_enabled: bool,
|
||||||
animation_speed: f64,
|
animation_speed: f64,
|
||||||
spring_config: SpringConfig,
|
spring_config: SpringConfig,
|
||||||
@@ -137,6 +138,7 @@ pub struct Ferese {
|
|||||||
|
|
||||||
pub struct RuntimeConfig {
|
pub struct RuntimeConfig {
|
||||||
pub layout_mode: LayoutMode,
|
pub layout_mode: LayoutMode,
|
||||||
|
pub gap_config: GapConfig,
|
||||||
pub default_column_width: ColumnWidth,
|
pub default_column_width: ColumnWidth,
|
||||||
pub scrolling_focus_strategy: ViewportFocusStrategy,
|
pub scrolling_focus_strategy: ViewportFocusStrategy,
|
||||||
pub column_width_presets: Vec<ColumnWidth>,
|
pub column_width_presets: Vec<ColumnWidth>,
|
||||||
@@ -363,6 +365,7 @@ impl Ferese {
|
|||||||
scrolling_world_x: HashMap::new(),
|
scrolling_world_x: HashMap::new(),
|
||||||
focused_window: None,
|
focused_window: None,
|
||||||
column_width_presets: config.column_width_presets,
|
column_width_presets: config.column_width_presets,
|
||||||
|
gap_config: config.gap_config,
|
||||||
animations_enabled: config.animations_enabled,
|
animations_enabled: config.animations_enabled,
|
||||||
animation_speed: config.animation_speed,
|
animation_speed: config.animation_speed,
|
||||||
spring_config: config.spring_config,
|
spring_config: config.spring_config,
|
||||||
@@ -699,7 +702,7 @@ impl Ferese {
|
|||||||
};
|
};
|
||||||
let layout = match workspace.layout.geometry_with_constraints(
|
let layout = match workspace.layout.geometry_with_constraints(
|
||||||
bounds,
|
bounds,
|
||||||
GapConfig::default(),
|
self.gap_config,
|
||||||
&constraints,
|
&constraints,
|
||||||
focused,
|
focused,
|
||||||
) {
|
) {
|
||||||
@@ -1162,7 +1165,7 @@ impl Ferese {
|
|||||||
|
|
||||||
match self
|
match self
|
||||||
.workspaces
|
.workspaces
|
||||||
.center_window(window, bounds, GapConfig::default(), &constraints)
|
.center_window(window, bounds, self.gap_config, &constraints)
|
||||||
{
|
{
|
||||||
Ok(true) => self.relayout(),
|
Ok(true) => self.relayout(),
|
||||||
Ok(false) => {}
|
Ok(false) => {}
|
||||||
@@ -1401,7 +1404,7 @@ impl Ferese {
|
|||||||
.workspaces
|
.workspaces
|
||||||
.workspace_mut(workspace)?
|
.workspace_mut(workspace)?
|
||||||
.layout
|
.layout
|
||||||
.geometry_with_constraints(bounds, GapConfig::default(), &constraints, focused)
|
.geometry_with_constraints(bounds, self.gap_config, &constraints, focused)
|
||||||
.ok()?
|
.ok()?
|
||||||
.geometry
|
.geometry
|
||||||
.get(&window)
|
.get(&window)
|
||||||
@@ -1416,7 +1419,7 @@ impl Ferese {
|
|||||||
self.workspaces
|
self.workspaces
|
||||||
.active_mut()
|
.active_mut()
|
||||||
.layout
|
.layout
|
||||||
.geometry_with_constraints(bounds, GapConfig::default(), &constraints, focused)
|
.geometry_with_constraints(bounds, self.gap_config, &constraints, focused)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
|
fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
|
||||||
|
|||||||
@@ -1025,7 +1025,10 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(first.geometry[&WindowId(1)].width, 1_000.0);
|
assert_eq!(
|
||||||
|
first.geometry[&WindowId(1)],
|
||||||
|
Rect::new(10.0, 10.0, 980.0, 780.0)
|
||||||
|
);
|
||||||
workspaces.switch_to_numeric(2).unwrap();
|
workspaces.switch_to_numeric(2).unwrap();
|
||||||
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling);
|
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ impl Default for GapConfig {
|
|||||||
Self {
|
Self {
|
||||||
inner: 10.0,
|
inner: 10.0,
|
||||||
outer: 10.0,
|
outer: 10.0,
|
||||||
smart: true,
|
smart: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1191,12 +1191,31 @@ mod tests {
|
|||||||
|
|
||||||
let bounds = Rect::new(0.0, 0.0, 100.0, 80.0);
|
let bounds = Rect::new(0.0, 0.0, 100.0, 80.0);
|
||||||
let geometry = tree
|
let geometry = tree
|
||||||
.geometry_with_gaps(bounds, GapConfig::default())
|
.geometry_with_gaps(
|
||||||
|
bounds,
|
||||||
|
GapConfig {
|
||||||
|
smart: true,
|
||||||
|
..GapConfig::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(geometry[&WindowId(1)], bounds);
|
assert_eq!(geometry[&WindowId(1)], bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_gaps_keep_outer_spacing_for_one_window() {
|
||||||
|
let mut tree = LayoutTree::default();
|
||||||
|
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let geometry = tree
|
||||||
|
.geometry_with_gaps(Rect::new(0.0, 0.0, 100.0, 80.0), GapConfig::default())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(geometry[&WindowId(1)], Rect::new(10.0, 10.0, 80.0, 60.0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn directional_neighbor_prefers_primary_distance() {
|
fn directional_neighbor_prefers_primary_distance() {
|
||||||
let mut tree = LayoutTree::default();
|
let mut tree = LayoutTree::default();
|
||||||
@@ -1552,7 +1571,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.geometry[&WindowId(1)],
|
result.geometry[&WindowId(1)],
|
||||||
Rect::new(0.0, 0.0, 60.0, 40.0)
|
Rect::new(10.0, 10.0, 60.0, 40.0)
|
||||||
);
|
);
|
||||||
assert_eq!(result.warnings.len(), 2);
|
assert_eq!(result.warnings.len(), 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -624,8 +624,9 @@ mode, center creates or joins a stack and an edge creates a split.
|
|||||||
|
|
||||||
Shared gap defaults live under `[layout]`; mode-specific behavior lives under
|
Shared gap defaults live under `[layout]`; mode-specific behavior lives under
|
||||||
`[scrolling]` and `[tiling]`. `inner_gap` separates managed windows and
|
`[scrolling]` and `[tiling]`. `inner_gap` separates managed windows and
|
||||||
`outer_gap` separates the layout from output bounds. With `smart_gaps = true`,
|
`outer_gap` separates the layout from output bounds. Outer gaps are retained by
|
||||||
a lone managed window has no outer gap.
|
default. With the explicit opt-in `smart_gaps = true`, a lone managed window has
|
||||||
|
no outer gap.
|
||||||
|
|
||||||
## 7. Geometry and animation
|
## 7. Geometry and animation
|
||||||
|
|
||||||
@@ -1083,7 +1084,7 @@ mass = 1.0
|
|||||||
mode = "scrolling"
|
mode = "scrolling"
|
||||||
inner_gap = 10.0
|
inner_gap = 10.0
|
||||||
outer_gap = 10.0
|
outer_gap = 10.0
|
||||||
smart_gaps = true
|
smart_gaps = false
|
||||||
|
|
||||||
[scrolling]
|
[scrolling]
|
||||||
default_column_width = 0.5
|
default_column_width = 0.5
|
||||||
|
|||||||
Reference in new issue
Block a user