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
@@ -11,6 +11,8 @@ ferese-core = { path = "../crates/ferese-core" }
|
||||
ferese-layout = { path = "../crates/ferese-layout" }
|
||||
libc.workspace = true
|
||||
smithay.workspace = true
|
||||
serde.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.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();
|
||||
}
|
||||
|
||||
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(())
|
||||
} else if modifiers.shift && symbol == keysyms::KEY_space {
|
||||
if state == KeyState::Pressed {
|
||||
@@ -313,6 +322,8 @@ impl Ferese {
|
||||
toplevel.send_pending_configure();
|
||||
}
|
||||
});
|
||||
|
||||
self.relayout();
|
||||
}
|
||||
|
||||
fn clamp_pointer_position(&self, requested: Point<f64, Logical>) -> Point<f64, Logical> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![allow(irrefutable_let_patterns)]
|
||||
|
||||
mod backends;
|
||||
mod config;
|
||||
mod cursor;
|
||||
mod grabs;
|
||||
mod handlers;
|
||||
@@ -15,15 +16,22 @@ pub use state::Ferese;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::backends::LaunchConfig;
|
||||
use crate::config::Config;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
init_logging();
|
||||
|
||||
let launch = LaunchConfig::from_environment()
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
|
||||
let config = Config::load()?;
|
||||
let mut event_loop = EventLoop::try_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)?;
|
||||
|
||||
// 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_core::{OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceSet};
|
||||
use ferese_layout::{Axis, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, WindowId};
|
||||
use ferese_core::{
|
||||
LayoutMode, OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceSet,
|
||||
};
|
||||
use ferese_layout::{
|
||||
Axis, ColumnWidth, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, WindowId,
|
||||
};
|
||||
|
||||
use smithay::{
|
||||
backend::{
|
||||
@@ -267,6 +271,8 @@ impl Ferese {
|
||||
pub fn new(
|
||||
event_loop: &mut EventLoop<'static, Self>,
|
||||
display: Display<Self>,
|
||||
default_layout_mode: LayoutMode,
|
||||
default_column_width: ColumnWidth,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
let display_handle = display.handle();
|
||||
crate::handlers::screencopy::init_global(&display_handle);
|
||||
@@ -315,7 +321,7 @@ impl Ferese {
|
||||
display_handle,
|
||||
loop_signal: event_loop.get_signal(),
|
||||
space: Space::default(),
|
||||
workspaces: WorkspaceSet::default(),
|
||||
workspaces: WorkspaceSet::new(default_layout_mode, default_column_width),
|
||||
output_workspaces: OutputWorkspaceMap::default(),
|
||||
output_ids: HashMap::new(),
|
||||
output_identity_ids: HashMap::new(),
|
||||
@@ -620,6 +626,7 @@ impl Ferese {
|
||||
pub fn relayout(&mut self) {
|
||||
self.arrange_layers();
|
||||
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
|
||||
let constraints = self.window_constraints();
|
||||
let mut visible = HashSet::new();
|
||||
let mut placements = Vec::new();
|
||||
|
||||
@@ -630,23 +637,28 @@ impl Ferese {
|
||||
let Some(workspace_id) = self.output_workspaces.active_workspace(output_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(workspace) = self.workspaces.workspace(workspace_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(bounds) = self.output_bounds_for(&output) else {
|
||||
continue;
|
||||
};
|
||||
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
|
||||
.focused_window
|
||||
.filter(|window| {
|
||||
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(
|
||||
bounds,
|
||||
GapConfig::default(),
|
||||
&self.window_constraints(),
|
||||
&constraints,
|
||||
focused,
|
||||
) {
|
||||
Ok(layout) => layout,
|
||||
@@ -671,9 +683,9 @@ impl Ferese {
|
||||
continue;
|
||||
}
|
||||
|
||||
let rect = if workspace.fullscreen == Some(*id) {
|
||||
let rect = if workspace_fullscreen == Some(*id) {
|
||||
fullscreen_bounds
|
||||
} else if workspace.fullscreen.is_some() {
|
||||
} else if workspace_fullscreen.is_some() {
|
||||
continue;
|
||||
} else {
|
||||
match self.workspaces.placement(*id) {
|
||||
@@ -687,7 +699,7 @@ impl Ferese {
|
||||
None => continue,
|
||||
}
|
||||
};
|
||||
let is_fullscreen = workspace.fullscreen == Some(*id);
|
||||
let is_fullscreen = workspace_fullscreen == Some(*id);
|
||||
let is_floating = matches!(
|
||||
self.workspaces.placement(*id),
|
||||
Some(WindowPlacement::Floating { .. })
|
||||
@@ -885,6 +897,8 @@ impl Ferese {
|
||||
toplevel.send_pending_configure();
|
||||
}
|
||||
}
|
||||
|
||||
self.relayout();
|
||||
}
|
||||
|
||||
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) {
|
||||
let Some(window) = self.focused_window else {
|
||||
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(workspace)?;
|
||||
let fullscreen = self.workspaces.workspace(workspace)?.fullscreen;
|
||||
|
||||
if workspace.fullscreen == Some(window) {
|
||||
if fullscreen == Some(window) {
|
||||
return Some(bounds);
|
||||
}
|
||||
|
||||
let constraints = self.window_constraints();
|
||||
let focused = self.focused_window;
|
||||
match self.workspaces.placement(window)? {
|
||||
WindowPlacement::Tiled => workspace
|
||||
WindowPlacement::Tiled => self
|
||||
.workspaces
|
||||
.workspace_mut(workspace)?
|
||||
.layout
|
||||
.geometry_with_constraints(
|
||||
bounds,
|
||||
GapConfig::default(),
|
||||
&self.window_constraints(),
|
||||
self.focused_window,
|
||||
)
|
||||
.geometry_with_constraints(bounds, GapConfig::default(), &constraints, focused)
|
||||
.ok()?
|
||||
.geometry
|
||||
.get(&window)
|
||||
@@ -1167,13 +1196,13 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn tiled_layout(&self, bounds: Rect) -> Result<LayoutResult, ferese_layout::LayoutError> {
|
||||
self.workspaces.active().layout.geometry_with_constraints(
|
||||
bounds,
|
||||
GapConfig::default(),
|
||||
&self.window_constraints(),
|
||||
self.focused_window,
|
||||
)
|
||||
fn tiled_layout(&mut self, bounds: Rect) -> Result<LayoutResult, ferese_layout::LayoutError> {
|
||||
let constraints = self.window_constraints();
|
||||
let focused = self.focused_window;
|
||||
self.workspaces
|
||||
.active_mut()
|
||||
.layout
|
||||
.geometry_with_constraints(bounds, GapConfig::default(), &constraints, focused)
|
||||
}
|
||||
|
||||
fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
|
||||
|
||||
Reference in new issue
Block a user