feat: honor tiled window size constraints
This commit is contained in:
1 parent
da4aaf9fb0
commit
a331355c8d
2 files changed
+480
-52
No files matched your search
+58
-16
@@ -7,7 +7,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use ferese_core::{WindowPlacement, WorkspaceSet};
|
use ferese_core::{WindowPlacement, WorkspaceSet};
|
||||||
use ferese_layout::{Axis, Direction, GapConfig, Rect, WindowId};
|
use ferese_layout::{Axis, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, WindowId};
|
||||||
|
|
||||||
use smithay::{
|
use smithay::{
|
||||||
desktop::{PopupManager, Space, Window, WindowSurfaceType},
|
desktop::{PopupManager, Space, Window, WindowSurfaceType},
|
||||||
@@ -23,15 +23,15 @@ use smithay::{
|
|||||||
},
|
},
|
||||||
utils::{Logical, Point, Size},
|
utils::{Logical, Point, Size},
|
||||||
wayland::{
|
wayland::{
|
||||||
compositor::{CompositorClientState, CompositorState},
|
compositor::{CompositorClientState, CompositorState, with_states},
|
||||||
fractional_scale::FractionalScaleManagerState,
|
fractional_scale::FractionalScaleManagerState,
|
||||||
output::OutputManagerState,
|
output::OutputManagerState,
|
||||||
pointer_constraints::PointerConstraintsState,
|
pointer_constraints::PointerConstraintsState,
|
||||||
presentation::PresentationState,
|
presentation::PresentationState,
|
||||||
relative_pointer::RelativePointerManagerState,
|
relative_pointer::RelativePointerManagerState,
|
||||||
selection::{data_device::DataDeviceState, primary_selection::PrimarySelectionState},
|
selection::{data_device::DataDeviceState, primary_selection::PrimarySelectionState},
|
||||||
shell::xdg::XdgShellState,
|
|
||||||
shell::xdg::decoration::XdgDecorationState,
|
shell::xdg::decoration::XdgDecorationState,
|
||||||
|
shell::xdg::{SurfaceCachedState, XdgShellState},
|
||||||
shm::ShmState,
|
shm::ShmState,
|
||||||
socket::ListeningSocketSource,
|
socket::ListeningSocketSource,
|
||||||
viewporter::ViewporterState,
|
viewporter::ViewporterState,
|
||||||
@@ -242,18 +242,24 @@ impl Ferese {
|
|||||||
let Some(bounds) = self.output_bounds() else {
|
let Some(bounds) = self.output_bounds() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let tiled_geometry = match self
|
let layout = match self.tiled_layout(bounds) {
|
||||||
.workspaces
|
Ok(layout) => layout,
|
||||||
.active()
|
|
||||||
.layout
|
|
||||||
.geometry_with_gaps(bounds, GapConfig::default())
|
|
||||||
{
|
|
||||||
Ok(geometry) => geometry,
|
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::error!(%error, "failed to compute tiled geometry");
|
tracing::error!(%error, "failed to compute tiled geometry");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let tiled_geometry = layout.geometry;
|
||||||
|
|
||||||
|
for warning in layout.warnings {
|
||||||
|
tracing::warn!(
|
||||||
|
window = ?warning.window,
|
||||||
|
kind = ?warning.kind,
|
||||||
|
requested = warning.requested,
|
||||||
|
assigned = warning.assigned,
|
||||||
|
"window size constraint could not be satisfied exactly"
|
||||||
|
);
|
||||||
|
}
|
||||||
let active = self.workspaces.active_id();
|
let active = self.workspaces.active_id();
|
||||||
let fullscreen = self.workspaces.active().fullscreen;
|
let fullscreen = self.workspaces.active().fullscreen;
|
||||||
let hidden = self
|
let hidden = self
|
||||||
@@ -419,12 +425,9 @@ impl Ferese {
|
|||||||
};
|
};
|
||||||
let floating_rect = match self.workspaces.placement(window) {
|
let floating_rect = match self.workspaces.placement(window) {
|
||||||
Some(WindowPlacement::Tiled) => self
|
Some(WindowPlacement::Tiled) => self
|
||||||
.workspaces
|
.tiled_layout(bounds)
|
||||||
.active()
|
|
||||||
.layout
|
|
||||||
.geometry_with_gaps(bounds, GapConfig::default())
|
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|geometry| geometry.get(&window).copied())
|
.and_then(|layout| layout.geometry.get(&window).copied())
|
||||||
.unwrap_or_else(|| centered_floating_rect(bounds)),
|
.unwrap_or_else(|| centered_floating_rect(bounds)),
|
||||||
Some(WindowPlacement::Floating { rect }) => rect,
|
Some(WindowPlacement::Floating { rect }) => rect,
|
||||||
None => return,
|
None => return,
|
||||||
@@ -580,13 +583,52 @@ impl Ferese {
|
|||||||
match self.workspaces.placement(window)? {
|
match self.workspaces.placement(window)? {
|
||||||
WindowPlacement::Tiled => workspace
|
WindowPlacement::Tiled => workspace
|
||||||
.layout
|
.layout
|
||||||
.geometry_with_gaps(bounds, GapConfig::default())
|
.geometry_with_constraints(
|
||||||
|
bounds,
|
||||||
|
GapConfig::default(),
|
||||||
|
&self.window_constraints(),
|
||||||
|
self.focused_window,
|
||||||
|
)
|
||||||
.ok()?
|
.ok()?
|
||||||
|
.geometry
|
||||||
.get(&window)
|
.get(&window)
|
||||||
.copied(),
|
.copied(),
|
||||||
WindowPlacement::Floating { rect } => Some(rect),
|
WindowPlacement::Floating { rect } => Some(rect),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
|
||||||
|
self.window_ids
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(window, id)| {
|
||||||
|
let toplevel = window.toplevel()?;
|
||||||
|
let (minimum, maximum) = with_states(toplevel.wl_surface(), |states| {
|
||||||
|
let mut cached = states.cached_state.get::<SurfaceCachedState>();
|
||||||
|
let state = cached.current();
|
||||||
|
(state.min_size, state.max_size)
|
||||||
|
});
|
||||||
|
|
||||||
|
Some((
|
||||||
|
*id,
|
||||||
|
SizeConstraints {
|
||||||
|
min_width: minimum.w.max(1) as f64,
|
||||||
|
min_height: minimum.h.max(1) as f64,
|
||||||
|
max_width: (maximum.w > 0).then_some(maximum.w as f64),
|
||||||
|
max_height: (maximum.h > 0).then_some(maximum.h as f64),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn centered_floating_rect(bounds: Rect) -> Rect {
|
fn centered_floating_rect(bounds: Rect) -> Rect {
|
||||||
|
|||||||
+422
-36
@@ -70,6 +70,47 @@ pub struct GapConfig {
|
|||||||
pub smart: bool,
|
pub smart: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct SizeConstraints {
|
||||||
|
pub min_width: f64,
|
||||||
|
pub min_height: f64,
|
||||||
|
pub max_width: Option<f64>,
|
||||||
|
pub max_height: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SizeConstraints {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
min_width: 1.0,
|
||||||
|
min_height: 1.0,
|
||||||
|
max_width: None,
|
||||||
|
max_height: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum ConstraintKind {
|
||||||
|
MinimumWidth,
|
||||||
|
MinimumHeight,
|
||||||
|
MaximumWidth,
|
||||||
|
MaximumHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct ConstraintWarning {
|
||||||
|
pub window: WindowId,
|
||||||
|
pub kind: ConstraintKind,
|
||||||
|
pub requested: f64,
|
||||||
|
pub assigned: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq)]
|
||||||
|
pub struct LayoutResult {
|
||||||
|
pub geometry: HashMap<WindowId, Rect>,
|
||||||
|
pub warnings: Vec<ConstraintWarning>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for GapConfig {
|
impl Default for GapConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -314,46 +355,69 @@ impl LayoutTree {
|
|||||||
gaps: GapConfig,
|
gaps: GapConfig,
|
||||||
) -> Result<HashMap<WindowId, Rect>, LayoutError> {
|
) -> Result<HashMap<WindowId, Rect>, LayoutError> {
|
||||||
let mut geometry = self.geometry(bounds)?;
|
let mut geometry = self.geometry(bounds)?;
|
||||||
let outer = if gaps.smart && geometry.len() == 1 {
|
apply_gaps(&mut geometry, bounds, gaps);
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
valid_gap(gaps.outer)
|
|
||||||
};
|
|
||||||
let half_inner = valid_gap(gaps.inner) / 2.0;
|
|
||||||
let right = bounds.x + bounds.width;
|
|
||||||
let bottom = bounds.y + bounds.height;
|
|
||||||
|
|
||||||
for rect in geometry.values_mut() {
|
|
||||||
let left_inset = if nearly_equal(rect.x, bounds.x) {
|
|
||||||
outer
|
|
||||||
} else {
|
|
||||||
half_inner
|
|
||||||
};
|
|
||||||
let top_inset = if nearly_equal(rect.y, bounds.y) {
|
|
||||||
outer
|
|
||||||
} else {
|
|
||||||
half_inner
|
|
||||||
};
|
|
||||||
let right_inset = if nearly_equal(rect.x + rect.width, right) {
|
|
||||||
outer
|
|
||||||
} else {
|
|
||||||
half_inner
|
|
||||||
};
|
|
||||||
let bottom_inset = if nearly_equal(rect.y + rect.height, bottom) {
|
|
||||||
outer
|
|
||||||
} else {
|
|
||||||
half_inner
|
|
||||||
};
|
|
||||||
|
|
||||||
rect.x += left_inset;
|
|
||||||
rect.y += top_inset;
|
|
||||||
rect.width = (rect.width - left_inset - right_inset).max(1.0);
|
|
||||||
rect.height = (rect.height - top_inset - bottom_inset).max(1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(geometry)
|
Ok(geometry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn geometry_with_constraints(
|
||||||
|
&self,
|
||||||
|
bounds: Rect,
|
||||||
|
gaps: GapConfig,
|
||||||
|
constraints: &HashMap<WindowId, SizeConstraints>,
|
||||||
|
focused: Option<WindowId>,
|
||||||
|
) -> Result<LayoutResult, LayoutError> {
|
||||||
|
let mut result = LayoutResult::default();
|
||||||
|
|
||||||
|
if let Some(root) = self.root {
|
||||||
|
self.layout_node_constrained(root, bounds, constraints, focused, &mut result.geometry)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_gaps(&mut result.geometry, bounds, gaps);
|
||||||
|
|
||||||
|
for (window, rect) in &mut result.geometry {
|
||||||
|
let constraint = constraints.get(window).copied().unwrap_or_default();
|
||||||
|
let constraint = normalized_constraints(constraint);
|
||||||
|
|
||||||
|
record_minimum_warnings(*window, *rect, constraint, &mut result.warnings);
|
||||||
|
|
||||||
|
if let Some(maximum) = constraint.max_width
|
||||||
|
&& rect.width > maximum
|
||||||
|
{
|
||||||
|
result.warnings.push(ConstraintWarning {
|
||||||
|
window: *window,
|
||||||
|
kind: ConstraintKind::MaximumWidth,
|
||||||
|
requested: maximum,
|
||||||
|
assigned: rect.width,
|
||||||
|
});
|
||||||
|
rect.width = maximum;
|
||||||
|
}
|
||||||
|
if let Some(maximum) = constraint.max_height
|
||||||
|
&& rect.height > maximum
|
||||||
|
{
|
||||||
|
result.warnings.push(ConstraintWarning {
|
||||||
|
window: *window,
|
||||||
|
kind: ConstraintKind::MaximumHeight,
|
||||||
|
requested: maximum,
|
||||||
|
assigned: rect.height,
|
||||||
|
});
|
||||||
|
rect.height = maximum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.warnings.sort_by_key(|warning| {
|
||||||
|
let kind = match warning.kind {
|
||||||
|
ConstraintKind::MinimumWidth => 0,
|
||||||
|
ConstraintKind::MinimumHeight => 1,
|
||||||
|
ConstraintKind::MaximumWidth => 2,
|
||||||
|
ConstraintKind::MaximumHeight => 3,
|
||||||
|
};
|
||||||
|
(warning.window.0, kind)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn automatic_axis(
|
pub fn automatic_axis(
|
||||||
&self,
|
&self,
|
||||||
focused: Option<WindowId>,
|
focused: Option<WindowId>,
|
||||||
@@ -665,6 +729,128 @@ impl LayoutTree {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn layout_node_constrained(
|
||||||
|
&self,
|
||||||
|
node: NodeId,
|
||||||
|
bounds: Rect,
|
||||||
|
constraints: &HashMap<WindowId, SizeConstraints>,
|
||||||
|
focused: Option<WindowId>,
|
||||||
|
geometry: &mut HashMap<WindowId, Rect>,
|
||||||
|
) -> Result<(), LayoutError> {
|
||||||
|
match self.nodes.get(&node) {
|
||||||
|
Some(Node::Window(window)) => {
|
||||||
|
geometry.insert(*window, bounds);
|
||||||
|
}
|
||||||
|
Some(Node::Split {
|
||||||
|
axis,
|
||||||
|
ratio,
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
}) => {
|
||||||
|
let first_minimum = self.minimum_size(*first, constraints)?;
|
||||||
|
let second_minimum = self.minimum_size(*second, constraints)?;
|
||||||
|
let available = match axis {
|
||||||
|
Axis::Horizontal => bounds.width,
|
||||||
|
Axis::Vertical => bounds.height,
|
||||||
|
}
|
||||||
|
.max(0.0);
|
||||||
|
let first_required = match axis {
|
||||||
|
Axis::Horizontal => first_minimum.0,
|
||||||
|
Axis::Vertical => first_minimum.1,
|
||||||
|
};
|
||||||
|
let second_required = match axis {
|
||||||
|
Axis::Horizontal => second_minimum.0,
|
||||||
|
Axis::Vertical => second_minimum.1,
|
||||||
|
};
|
||||||
|
let desired = available * ratio;
|
||||||
|
let first_extent = if first_required + second_required <= available {
|
||||||
|
desired.clamp(first_required, available - second_required)
|
||||||
|
} else if focused.is_some_and(|window| self.contains_window(*first, window)) {
|
||||||
|
first_required.min(available)
|
||||||
|
} else {
|
||||||
|
(available - second_required).max(0.0)
|
||||||
|
};
|
||||||
|
let adjusted_ratio = if available > 0.0 {
|
||||||
|
first_extent / available
|
||||||
|
} else {
|
||||||
|
0.5
|
||||||
|
};
|
||||||
|
let (first_bounds, second_bounds) = split_rect(bounds, *axis, adjusted_ratio);
|
||||||
|
|
||||||
|
self.layout_node_constrained(*first, first_bounds, constraints, focused, geometry)?;
|
||||||
|
self.layout_node_constrained(
|
||||||
|
*second,
|
||||||
|
second_bounds,
|
||||||
|
constraints,
|
||||||
|
focused,
|
||||||
|
geometry,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Some(Node::Stack { children, active }) => {
|
||||||
|
let child = children
|
||||||
|
.get(*active)
|
||||||
|
.ok_or(LayoutError::InvalidTree("stack active index is invalid"))?;
|
||||||
|
self.layout_node_constrained(*child, bounds, constraints, focused, geometry)?;
|
||||||
|
}
|
||||||
|
None => return Err(LayoutError::InvalidTree("node is missing")),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn minimum_size(
|
||||||
|
&self,
|
||||||
|
node: NodeId,
|
||||||
|
constraints: &HashMap<WindowId, SizeConstraints>,
|
||||||
|
) -> Result<(f64, f64), LayoutError> {
|
||||||
|
match self.nodes.get(&node) {
|
||||||
|
Some(Node::Window(window)) => {
|
||||||
|
let constraint = constraints.get(window).copied().unwrap_or_default();
|
||||||
|
let constraint = normalized_constraints(constraint);
|
||||||
|
Ok((constraint.min_width, constraint.min_height))
|
||||||
|
}
|
||||||
|
Some(Node::Split {
|
||||||
|
axis,
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let first = self.minimum_size(*first, constraints)?;
|
||||||
|
let second = self.minimum_size(*second, constraints)?;
|
||||||
|
|
||||||
|
Ok(match axis {
|
||||||
|
Axis::Horizontal => (first.0 + second.0, first.1.max(second.1)),
|
||||||
|
Axis::Vertical => (first.0.max(second.0), first.1 + second.1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Some(Node::Stack { children, .. }) => {
|
||||||
|
let mut minimum = (1.0_f64, 1.0_f64);
|
||||||
|
|
||||||
|
for child in children {
|
||||||
|
let child = self.minimum_size(*child, constraints)?;
|
||||||
|
minimum.0 = minimum.0.max(child.0);
|
||||||
|
minimum.1 = minimum.1.max(child.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(minimum)
|
||||||
|
}
|
||||||
|
None => Err(LayoutError::InvalidTree("node is missing")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_window(&self, node: NodeId, window: WindowId) -> bool {
|
||||||
|
match self.nodes.get(&node) {
|
||||||
|
Some(Node::Window(candidate)) => *candidate == window,
|
||||||
|
Some(Node::Split { first, second, .. }) => {
|
||||||
|
self.contains_window(*first, window) || self.contains_window(*second, window)
|
||||||
|
}
|
||||||
|
Some(Node::Stack { children, .. }) => children
|
||||||
|
.iter()
|
||||||
|
.any(|child| self.contains_window(*child, window)),
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_node(
|
fn validate_node(
|
||||||
&self,
|
&self,
|
||||||
node: NodeId,
|
node: NodeId,
|
||||||
@@ -736,6 +922,93 @@ fn valid_resize_amount(amount: f64) -> f64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalized_constraints(constraints: SizeConstraints) -> SizeConstraints {
|
||||||
|
let min_width = finite_positive(constraints.min_width).unwrap_or(1.0);
|
||||||
|
let min_height = finite_positive(constraints.min_height).unwrap_or(1.0);
|
||||||
|
let max_width = constraints
|
||||||
|
.max_width
|
||||||
|
.and_then(finite_positive)
|
||||||
|
.map(|maximum| maximum.max(min_width));
|
||||||
|
let max_height = constraints
|
||||||
|
.max_height
|
||||||
|
.and_then(finite_positive)
|
||||||
|
.map(|maximum| maximum.max(min_height));
|
||||||
|
|
||||||
|
SizeConstraints {
|
||||||
|
min_width,
|
||||||
|
min_height,
|
||||||
|
max_width,
|
||||||
|
max_height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finite_positive(value: f64) -> Option<f64> {
|
||||||
|
(value.is_finite() && value > 0.0).then_some(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_minimum_warnings(
|
||||||
|
window: WindowId,
|
||||||
|
rect: Rect,
|
||||||
|
constraints: SizeConstraints,
|
||||||
|
warnings: &mut Vec<ConstraintWarning>,
|
||||||
|
) {
|
||||||
|
if rect.width < constraints.min_width {
|
||||||
|
warnings.push(ConstraintWarning {
|
||||||
|
window,
|
||||||
|
kind: ConstraintKind::MinimumWidth,
|
||||||
|
requested: constraints.min_width,
|
||||||
|
assigned: rect.width,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if rect.height < constraints.min_height {
|
||||||
|
warnings.push(ConstraintWarning {
|
||||||
|
window,
|
||||||
|
kind: ConstraintKind::MinimumHeight,
|
||||||
|
requested: constraints.min_height,
|
||||||
|
assigned: rect.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_gaps(geometry: &mut HashMap<WindowId, Rect>, bounds: Rect, gaps: GapConfig) {
|
||||||
|
let outer = if gaps.smart && geometry.len() == 1 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
valid_gap(gaps.outer)
|
||||||
|
};
|
||||||
|
let half_inner = valid_gap(gaps.inner) / 2.0;
|
||||||
|
let right = bounds.x + bounds.width;
|
||||||
|
let bottom = bounds.y + bounds.height;
|
||||||
|
|
||||||
|
for rect in geometry.values_mut() {
|
||||||
|
let left_inset = if nearly_equal(rect.x, bounds.x) {
|
||||||
|
outer
|
||||||
|
} else {
|
||||||
|
half_inner
|
||||||
|
};
|
||||||
|
let top_inset = if nearly_equal(rect.y, bounds.y) {
|
||||||
|
outer
|
||||||
|
} else {
|
||||||
|
half_inner
|
||||||
|
};
|
||||||
|
let right_inset = if nearly_equal(rect.x + rect.width, right) {
|
||||||
|
outer
|
||||||
|
} else {
|
||||||
|
half_inner
|
||||||
|
};
|
||||||
|
let bottom_inset = if nearly_equal(rect.y + rect.height, bottom) {
|
||||||
|
outer
|
||||||
|
} else {
|
||||||
|
half_inner
|
||||||
|
};
|
||||||
|
|
||||||
|
rect.x += left_inset;
|
||||||
|
rect.y += top_inset;
|
||||||
|
rect.width = (rect.width - left_inset - right_inset).max(1.0);
|
||||||
|
rect.height = (rect.height - top_inset - bottom_inset).max(1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn nearly_equal(left: f64, right: f64) -> bool {
|
fn nearly_equal(left: f64, right: f64) -> bool {
|
||||||
(left - right).abs() < f64::EPSILON * left.abs().max(right.abs()).max(1.0)
|
(left - right).abs() < f64::EPSILON * left.abs().max(right.abs()).max(1.0)
|
||||||
}
|
}
|
||||||
@@ -1155,6 +1428,119 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn constraints_adjust_split_boundaries_when_space_is_available() {
|
||||||
|
let mut tree = LayoutTree::default();
|
||||||
|
tree.insert(WindowId(1), None, Axis::Horizontal, 0.2)
|
||||||
|
.unwrap();
|
||||||
|
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.2)
|
||||||
|
.unwrap();
|
||||||
|
let constraints = HashMap::from([
|
||||||
|
(
|
||||||
|
WindowId(1),
|
||||||
|
SizeConstraints {
|
||||||
|
min_width: 40.0,
|
||||||
|
..SizeConstraints::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WindowId(2),
|
||||||
|
SizeConstraints {
|
||||||
|
min_width: 30.0,
|
||||||
|
..SizeConstraints::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
let result = tree
|
||||||
|
.geometry_with_constraints(
|
||||||
|
Rect::new(0.0, 0.0, 100.0, 80.0),
|
||||||
|
GapConfig {
|
||||||
|
inner: 0.0,
|
||||||
|
outer: 0.0,
|
||||||
|
smart: false,
|
||||||
|
},
|
||||||
|
&constraints,
|
||||||
|
Some(WindowId(1)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.geometry[&WindowId(1)].width, 40.0);
|
||||||
|
assert_eq!(result.geometry[&WindowId(2)].width, 60.0);
|
||||||
|
assert!(result.warnings.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focused_window_wins_an_impossible_minimum_constraint_conflict() {
|
||||||
|
let mut tree = LayoutTree::default();
|
||||||
|
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
let constraints = HashMap::from([
|
||||||
|
(
|
||||||
|
WindowId(1),
|
||||||
|
SizeConstraints {
|
||||||
|
min_width: 80.0,
|
||||||
|
..SizeConstraints::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WindowId(2),
|
||||||
|
SizeConstraints {
|
||||||
|
min_width: 80.0,
|
||||||
|
..SizeConstraints::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
let result = tree
|
||||||
|
.geometry_with_constraints(
|
||||||
|
Rect::new(0.0, 0.0, 100.0, 80.0),
|
||||||
|
GapConfig {
|
||||||
|
inner: 0.0,
|
||||||
|
outer: 0.0,
|
||||||
|
smart: false,
|
||||||
|
},
|
||||||
|
&constraints,
|
||||||
|
Some(WindowId(1)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.geometry[&WindowId(1)].width, 80.0);
|
||||||
|
assert_eq!(result.geometry[&WindowId(2)].width, 20.0);
|
||||||
|
assert_eq!(result.warnings.len(), 1);
|
||||||
|
assert_eq!(result.warnings[0].window, WindowId(2));
|
||||||
|
assert_eq!(result.warnings[0].kind, ConstraintKind::MinimumWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maximum_constraints_clip_without_overlapping_neighbors() {
|
||||||
|
let mut tree = LayoutTree::default();
|
||||||
|
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
let constraints = HashMap::from([(
|
||||||
|
WindowId(1),
|
||||||
|
SizeConstraints {
|
||||||
|
max_width: Some(60.0),
|
||||||
|
max_height: Some(40.0),
|
||||||
|
..SizeConstraints::default()
|
||||||
|
},
|
||||||
|
)]);
|
||||||
|
let result = tree
|
||||||
|
.geometry_with_constraints(
|
||||||
|
Rect::new(0.0, 0.0, 100.0, 80.0),
|
||||||
|
GapConfig::default(),
|
||||||
|
&constraints,
|
||||||
|
Some(WindowId(1)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result.geometry[&WindowId(1)],
|
||||||
|
Rect::new(0.0, 0.0, 60.0, 40.0)
|
||||||
|
);
|
||||||
|
assert_eq!(result.warnings.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
fn choose_window(windows: &[WindowId], random: u64) -> Option<WindowId> {
|
fn choose_window(windows: &[WindowId], random: u64) -> Option<WindowId> {
|
||||||
(!windows.is_empty()).then(|| windows[random as usize % windows.len()])
|
(!windows.is_empty()).then(|| windows[random as usize % windows.len()])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user