Fix compositor output routing and pointer hints; format imports
This commit is contained in:
1 parent
88e4f3c135
commit
46509f880a
108 files changed
+3682
-8371
No files matched your search
@@ -108,13 +108,7 @@ impl AnimatedValue {
|
||||
|
||||
for _ in 0..steps {
|
||||
let previous_error = self.current - self.target;
|
||||
advance_value(
|
||||
&mut self.current,
|
||||
self.target,
|
||||
&mut self.velocity,
|
||||
step,
|
||||
config,
|
||||
);
|
||||
advance_value(&mut self.current, self.target, &mut self.velocity, step, config);
|
||||
let current_error = self.current - self.target;
|
||||
if previous_error != 0.0 && previous_error.signum() != current_error.signum() {
|
||||
self.snap();
|
||||
@@ -163,20 +157,8 @@ impl AnimatedRect {
|
||||
let step = seconds / steps as f64;
|
||||
|
||||
for _ in 0..steps {
|
||||
advance_value(
|
||||
&mut self.current.x,
|
||||
self.target.x,
|
||||
&mut self.velocity.x,
|
||||
step,
|
||||
config,
|
||||
);
|
||||
advance_value(
|
||||
&mut self.current.y,
|
||||
self.target.y,
|
||||
&mut self.velocity.y,
|
||||
step,
|
||||
config,
|
||||
);
|
||||
advance_value(&mut self.current.x, self.target.x, &mut self.velocity.x, step, config);
|
||||
advance_value(&mut self.current.y, self.target.y, &mut self.velocity.y, step, config);
|
||||
advance_value(
|
||||
&mut self.current.width,
|
||||
self.target.width,
|
||||
@@ -321,12 +303,7 @@ impl WindowGeometry {
|
||||
self.client.request_size(size, now).then_some(size)
|
||||
}
|
||||
|
||||
pub fn set_presentation_target(
|
||||
&mut self,
|
||||
rect: Rect,
|
||||
fullscreen: bool,
|
||||
now: Duration,
|
||||
) -> Option<ClientSize> {
|
||||
pub fn set_presentation_target(&mut self, rect: Rect, fullscreen: bool, now: Duration) -> Option<ClientSize> {
|
||||
self.set_presentation_mode(
|
||||
rect,
|
||||
if fullscreen {
|
||||
@@ -338,12 +315,7 @@ impl WindowGeometry {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_presentation_mode(
|
||||
&mut self,
|
||||
rect: Rect,
|
||||
mode: PresentationMode,
|
||||
now: Duration,
|
||||
) -> Option<ClientSize> {
|
||||
pub fn set_presentation_mode(&mut self, rect: Rect, mode: PresentationMode, now: Duration) -> Option<ClientSize> {
|
||||
let rect = normalized_rect(rect);
|
||||
if self.mode != mode || (self.zoom.is_some() && self.logical != rect) {
|
||||
let mut progress = AnimatedValue::new(0.0);
|
||||
@@ -359,14 +331,9 @@ impl WindowGeometry {
|
||||
let speed = [velocity.x, velocity.y, velocity.width, velocity.height];
|
||||
let squared_length = distance.iter().map(|value| value * value).sum::<f64>();
|
||||
if squared_length > 0.001 {
|
||||
let projected =
|
||||
distance.iter().zip(speed).map(|(d, v)| d * v).sum::<f64>() / squared_length;
|
||||
let projected = distance.iter().zip(speed).map(|(d, v)| d * v).sum::<f64>() / squared_length;
|
||||
// Keep forward momentum; soften a reversal without jumping position.
|
||||
progress.velocity = if projected < 0.0 {
|
||||
projected * 0.35
|
||||
} else {
|
||||
projected
|
||||
};
|
||||
progress.velocity = if projected < 0.0 { projected * 0.35 } else { projected };
|
||||
}
|
||||
self.zoom = Some(ZoomTransition {
|
||||
from: self.visual.current,
|
||||
@@ -412,24 +379,14 @@ impl WindowGeometry {
|
||||
requested_size
|
||||
}
|
||||
|
||||
pub fn advance(
|
||||
&mut self,
|
||||
delta: Duration,
|
||||
config: SpringConfig,
|
||||
animations_enabled: bool,
|
||||
) -> bool {
|
||||
pub fn advance(&mut self, delta: Duration, config: SpringConfig, animations_enabled: bool) -> bool {
|
||||
let previous = (self.visual.current, self.decorations);
|
||||
let active = self.advance_presentation(delta, config, animations_enabled);
|
||||
self.presentation_changed = previous != (self.visual.current, self.decorations);
|
||||
active
|
||||
}
|
||||
|
||||
fn advance_presentation(
|
||||
&mut self,
|
||||
delta: Duration,
|
||||
config: SpringConfig,
|
||||
animations_enabled: bool,
|
||||
) -> bool {
|
||||
fn advance_presentation(&mut self, delta: Duration, config: SpringConfig, animations_enabled: bool) -> bool {
|
||||
if !animations_enabled {
|
||||
self.visual.snap();
|
||||
self.decorations = if self.is_fullscreen() { 0.0 } else { 1.0 };
|
||||
@@ -506,15 +463,8 @@ impl WindowGeometry {
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_value(
|
||||
current: &mut f64,
|
||||
target: f64,
|
||||
velocity: &mut f64,
|
||||
delta: f64,
|
||||
config: SpringConfig,
|
||||
) {
|
||||
let acceleration =
|
||||
(-config.stiffness * (*current - target) - config.damping * *velocity) / config.mass;
|
||||
fn advance_value(current: &mut f64, target: f64, velocity: &mut f64, delta: f64, config: SpringConfig) {
|
||||
let acceleration = (-config.stiffness * (*current - target) - config.damping * *velocity) / config.mass;
|
||||
*velocity += acceleration * delta;
|
||||
let previous_error = *current - target;
|
||||
*current += *velocity * delta;
|
||||
@@ -641,17 +591,10 @@ mod tests {
|
||||
assert_eq!(geometry.visual.current, rect);
|
||||
assert_eq!(
|
||||
geometry.decorations,
|
||||
if mode == PresentationMode::Fullscreen {
|
||||
0.0
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
if mode == PresentationMode::Fullscreen { 0.0 } else { 1.0 }
|
||||
);
|
||||
assert!(!geometry.is_zooming());
|
||||
assert_eq!(
|
||||
geometry.is_fullscreen(),
|
||||
mode == PresentationMode::Fullscreen
|
||||
);
|
||||
assert_eq!(geometry.is_fullscreen(), mode == PresentationMode::Fullscreen);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,9 +621,7 @@ mod tests {
|
||||
fn zoom_finishes_without_a_visible_final_edge_jump() {
|
||||
let tiled = Rect::new(12.0, 12.0, 942.0, 1056.0);
|
||||
let fullscreen = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
||||
for (start, target, fullscreen_target) in
|
||||
[(tiled, fullscreen, true), (fullscreen, tiled, false)]
|
||||
{
|
||||
for (start, target, fullscreen_target) in [(tiled, fullscreen, true), (fullscreen, tiled, false)] {
|
||||
let mut geometry = WindowGeometry::new(start, None);
|
||||
if !fullscreen_target {
|
||||
geometry.set_presentation_target(start, true, Duration::ZERO);
|
||||
@@ -690,11 +631,7 @@ mod tests {
|
||||
let mut finished = false;
|
||||
for _ in 0..1000 {
|
||||
let before = geometry.visual.current;
|
||||
if !geometry.advance(
|
||||
Duration::from_secs_f64(1.0 / 144.0),
|
||||
SpringConfig::default(),
|
||||
true,
|
||||
) {
|
||||
if !geometry.advance(Duration::from_secs_f64(1.0 / 144.0), SpringConfig::default(), true) {
|
||||
let right_before = before.x + before.width;
|
||||
let right_after = target.x + target.width;
|
||||
assert!(
|
||||
@@ -721,9 +658,7 @@ mod tests {
|
||||
let progress = 1.0 - geometry.decorations;
|
||||
let visual = geometry.visual.current;
|
||||
assert!((visual.x - (start.x + (end.x - start.x) * progress)).abs() < 1e-9);
|
||||
assert!(
|
||||
(visual.width - (start.width + (end.width - start.width) * progress)).abs() < 1e-9
|
||||
);
|
||||
assert!((visual.width - (start.width + (end.width - start.width) * progress)).abs() < 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,11 +669,7 @@ mod tests {
|
||||
let mut geometry = WindowGeometry::new(tiled, None);
|
||||
for enabled in [true, false, true, false] {
|
||||
let before = (geometry.visual.current, geometry.decorations);
|
||||
geometry.set_presentation_target(
|
||||
if enabled { fullscreen } else { tiled },
|
||||
enabled,
|
||||
Duration::ZERO,
|
||||
);
|
||||
geometry.set_presentation_target(if enabled { fullscreen } else { tiled }, enabled, Duration::ZERO);
|
||||
assert_eq!((geometry.visual.current, geometry.decorations), before);
|
||||
geometry.advance(Duration::from_millis(48), SpringConfig::default(), true);
|
||||
}
|
||||
@@ -782,24 +713,15 @@ mod tests {
|
||||
Some(ClientSize::from_rect(middle))
|
||||
);
|
||||
geometry.advance(Duration::from_millis(64), SpringConfig::default(), true);
|
||||
assert_eq!(geometry.presentation_size_request(Duration::from_millis(64)), None);
|
||||
assert_eq!(
|
||||
geometry.presentation_size_request(Duration::from_millis(64)),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
geometry.set_presentation_mode(
|
||||
end,
|
||||
PresentationMode::Normal,
|
||||
Duration::from_millis(64)
|
||||
),
|
||||
geometry.set_presentation_mode(end, PresentationMode::Normal, Duration::from_millis(64)),
|
||||
Some(ClientSize::from_rect(end))
|
||||
);
|
||||
let mut requests = Vec::new();
|
||||
for frame in 1..240 {
|
||||
geometry.advance(Duration::from_millis(8), SpringConfig::default(), true);
|
||||
if let Some(size) =
|
||||
geometry.presentation_size_request(Duration::from_millis(64 + frame * 8))
|
||||
{
|
||||
if let Some(size) = geometry.presentation_size_request(Duration::from_millis(64 + frame * 8)) {
|
||||
assert_eq!(geometry.visual.current.width, end.width);
|
||||
requests.push(size);
|
||||
}
|
||||
@@ -844,13 +766,8 @@ mod tests {
|
||||
true,
|
||||
);
|
||||
let current = geometry.visual.current;
|
||||
let edges = |rect: Rect| {
|
||||
[rect.x, rect.y, rect.x + rect.width, rect.y + rect.height]
|
||||
};
|
||||
for ((before, after), end) in edges(previous)
|
||||
.into_iter()
|
||||
.zip(edges(current))
|
||||
.zip(edges(target))
|
||||
let edges = |rect: Rect| [rect.x, rect.y, rect.x + rect.width, rect.y + rect.height];
|
||||
for ((before, after), end) in edges(previous).into_iter().zip(edges(current)).zip(edges(target))
|
||||
{
|
||||
assert!((after - end).abs() <= (before - end).abs() + 1e-9);
|
||||
}
|
||||
@@ -1004,11 +921,7 @@ mod tests {
|
||||
let mut geometry = WindowGeometry::new(rect, None);
|
||||
|
||||
assert!(geometry.set_logical_target(rect, Duration::ZERO).is_some());
|
||||
assert!(
|
||||
geometry
|
||||
.set_logical_target(rect, Duration::from_millis(16))
|
||||
.is_none()
|
||||
);
|
||||
assert!(geometry.set_logical_target(rect, Duration::from_millis(16)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1067,10 +980,7 @@ mod tests {
|
||||
assert!(sixty.current.width <= target.width);
|
||||
}
|
||||
for _ in 0..288 {
|
||||
one_forty_four.advance(
|
||||
Duration::from_secs_f64(1.0 / 144.0),
|
||||
SpringConfig::default(),
|
||||
);
|
||||
one_forty_four.advance(Duration::from_secs_f64(1.0 / 144.0), SpringConfig::default());
|
||||
}
|
||||
|
||||
assert!(sixty.is_settled(SpringConfig::default()));
|
||||
|
||||
+36
-119
@@ -1,7 +1,9 @@
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::{Map, Value};
|
||||
use std::{fmt, path::PathBuf};
|
||||
|
||||
pub const DEFAULT_MATERIAL_OPACITY: f64 = 0.78;
|
||||
|
||||
@@ -36,14 +38,9 @@ pub fn default_wallpaper() -> &'static str {
|
||||
.ok()
|
||||
.and_then(|path| path.parent().map(|dir| dir.join("wallpapers/ferese.png")))
|
||||
.filter(|path| path.is_file())
|
||||
.unwrap_or_else(|| {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/wallpapers/ferese.png")
|
||||
});
|
||||
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/wallpapers/ferese.png"));
|
||||
|
||||
path.canonicalize()
|
||||
.unwrap_or(path)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
path.canonicalize().unwrap_or(path).to_string_lossy().into_owned()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -65,10 +62,8 @@ fn field(name: &str, parent: &str) -> String {
|
||||
fn is_records(key: &str, parent: &str) -> bool {
|
||||
matches!(
|
||||
(parent, key),
|
||||
(
|
||||
"",
|
||||
"bindings" | "window_rules" | "output_profiles" | "autostart"
|
||||
) | ("output_profiles", "outputs")
|
||||
("", "bindings" | "window_rules" | "output_profiles" | "autostart")
|
||||
| ("output_profiles", "outputs")
|
||||
| ("desktop_widgets", "notes")
|
||||
)
|
||||
}
|
||||
@@ -77,12 +72,7 @@ fn is_array(key: &str, parent: &str) -> bool {
|
||||
parent == "commands"
|
||||
|| matches!(
|
||||
key,
|
||||
"command"
|
||||
| "position"
|
||||
| "width_presets"
|
||||
| "xkb_options"
|
||||
| "settings_command"
|
||||
| "outputs"
|
||||
"command" | "position" | "width_presets" | "xkb_options" | "settings_command" | "outputs"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -91,9 +81,7 @@ fn scalar(value: &KdlValue) -> Result<Value, Error> {
|
||||
KdlValue::String(s) => Value::String(s.clone()),
|
||||
KdlValue::Bool(v) => Value::Bool(*v),
|
||||
KdlValue::Null => {
|
||||
return Err(Error(
|
||||
"Omit optional settings instead of using #null".into(),
|
||||
));
|
||||
return Err(Error("Omit optional settings instead of using #null".into()));
|
||||
}
|
||||
KdlValue::Integer(n) => {
|
||||
if let Ok(n) = i64::try_from(*n) {
|
||||
@@ -124,9 +112,7 @@ fn object(doc: &KdlDocument, parent: &str) -> Result<Map<String, Value>, Error>
|
||||
let key = field(node.name().value(), parent);
|
||||
let value = node_value(node, &key, parent)?;
|
||||
if is_records(&key, parent) {
|
||||
let records = result
|
||||
.entry(key)
|
||||
.or_insert_with(|| Value::Array(Vec::new()));
|
||||
let records = result.entry(key).or_insert_with(|| Value::Array(Vec::new()));
|
||||
records.as_array_mut().unwrap().push(value);
|
||||
} else {
|
||||
insert(&mut result, key, value)?;
|
||||
@@ -138,9 +124,7 @@ fn object(doc: &KdlDocument, parent: &str) -> Result<Map<String, Value>, Error>
|
||||
|
||||
fn node_value(node: &KdlNode, key: &str, parent: &str) -> Result<Value, Error> {
|
||||
if node.ty().is_some() || node.entries().iter().any(|e| e.ty().is_some()) {
|
||||
return Err(Error(
|
||||
"Type annotations are not supported in configuration".into(),
|
||||
));
|
||||
return Err(Error("Type annotations are not supported in configuration".into()));
|
||||
}
|
||||
|
||||
let args = node
|
||||
@@ -156,11 +140,7 @@ fn node_value(node: &KdlNode, key: &str, parent: &str) -> Result<Value, Error> {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if is_records(key, parent) {
|
||||
let mut map = node
|
||||
.children()
|
||||
.map(|d| object(d, key))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
let mut map = node.children().map(|d| object(d, key)).transpose()?.unwrap_or_default();
|
||||
for (name, entry) in props {
|
||||
insert(&mut map, name, scalar(entry.value())?)?;
|
||||
}
|
||||
@@ -195,11 +175,7 @@ fn node_value(node: &KdlNode, key: &str, parent: &str) -> Result<Value, Error> {
|
||||
node.name()
|
||||
)));
|
||||
}
|
||||
let mut map = node
|
||||
.children()
|
||||
.map(|d| object(d, key))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
let mut map = node.children().map(|d| object(d, key)).transpose()?.unwrap_or_default();
|
||||
for (name, entry) in props {
|
||||
insert(&mut map, name, scalar(entry.value())?)?;
|
||||
}
|
||||
@@ -224,9 +200,7 @@ pub struct Document {
|
||||
|
||||
impl Document {
|
||||
pub fn parse(source: &str) -> Result<Self, Error> {
|
||||
let doc = source
|
||||
.parse::<KdlDocument>()
|
||||
.map_err(|e| Error(format!("{e:?}")))?;
|
||||
let doc = source.parse::<KdlDocument>().map_err(|e| Error(format!("{e:?}")))?;
|
||||
let value = Value::Object(object(&doc, "")?);
|
||||
|
||||
Ok(Self { doc, value })
|
||||
@@ -267,10 +241,7 @@ impl Document {
|
||||
pub fn add(&mut self, path: &str, fields: Vec<(String, Value)>) -> Result<(), Error> {
|
||||
let mut candidate = self.clone();
|
||||
let (last, parents) = path.rsplit_once('.').map_or((path, ""), |(p, l)| (l, p));
|
||||
let parent = parents
|
||||
.rsplit('.')
|
||||
.find(|s| s.parse::<usize>().is_err())
|
||||
.unwrap_or("");
|
||||
let parent = parents.rsplit('.').find(|s| s.parse::<usize>().is_err()).unwrap_or("");
|
||||
|
||||
if !is_records(last, parent) {
|
||||
return Err(Error("Expected a list of records".into()));
|
||||
@@ -278,18 +249,12 @@ impl Document {
|
||||
|
||||
let doc = section_mut(
|
||||
&mut candidate.doc,
|
||||
&parents
|
||||
.split('.')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>(),
|
||||
&parents.split('.').filter(|s| !s.is_empty()).collect::<Vec<_>>(),
|
||||
"",
|
||||
)?;
|
||||
|
||||
doc.nodes_mut().push(value_node(
|
||||
last,
|
||||
&Value::Object(fields.into_iter().collect()),
|
||||
parent,
|
||||
)?);
|
||||
doc.nodes_mut()
|
||||
.push(value_node(last, &Value::Object(fields.into_iter().collect()), parent)?);
|
||||
format_document(&mut candidate.doc);
|
||||
candidate.refresh()?;
|
||||
*self = candidate;
|
||||
@@ -302,19 +267,14 @@ impl Document {
|
||||
let (last, parents) = path.rsplit_once('.').map_or((path, ""), |(p, l)| (l, p));
|
||||
let doc = section_mut(
|
||||
&mut candidate.doc,
|
||||
&parents
|
||||
.split('.')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>(),
|
||||
&parents.split('.').filter(|s| !s.is_empty()).collect::<Vec<_>>(),
|
||||
"",
|
||||
)?;
|
||||
let i = doc
|
||||
.nodes()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, n)| {
|
||||
field(n.name().value(), parents.rsplit('.').next().unwrap_or("")) == last
|
||||
})
|
||||
.filter(|(_, n)| field(n.name().value(), parents.rsplit('.').next().unwrap_or("")) == last)
|
||||
.nth(index)
|
||||
.map(|(i, _)| i)
|
||||
.ok_or_else(|| Error("This item no longer exists. Reload settings.".into()))?;
|
||||
@@ -346,11 +306,7 @@ type NodeFormat = (Vec<Option<kdl::KdlEntryFormat>>, Option<String>);
|
||||
|
||||
fn collect_node_formats(doc: &KdlDocument, formats: &mut Vec<NodeFormat>) {
|
||||
for node in doc.nodes() {
|
||||
let entries = node
|
||||
.entries()
|
||||
.iter()
|
||||
.map(|entry| entry.format().cloned())
|
||||
.collect();
|
||||
let entries = node.entries().iter().map(|entry| entry.format().cloned()).collect();
|
||||
let terminator = node
|
||||
.format()
|
||||
.map(|format| format.terminator.clone())
|
||||
@@ -391,11 +347,7 @@ fn format_document(doc: &mut KdlDocument) {
|
||||
}
|
||||
|
||||
fn node_index(doc: &mut KdlDocument, key: &str, parent: &str) -> usize {
|
||||
if let Some(i) = doc
|
||||
.nodes()
|
||||
.iter()
|
||||
.position(|n| field(n.name().value(), parent) == key)
|
||||
{
|
||||
if let Some(i) = doc.nodes().iter().position(|n| field(n.name().value(), parent) == key) {
|
||||
return i;
|
||||
}
|
||||
|
||||
@@ -403,11 +355,7 @@ fn node_index(doc: &mut KdlDocument, key: &str, parent: &str) -> usize {
|
||||
doc.nodes().len() - 1
|
||||
}
|
||||
|
||||
fn section_mut<'a>(
|
||||
doc: &'a mut KdlDocument,
|
||||
parts: &[&str],
|
||||
parent: &str,
|
||||
) -> Result<&'a mut KdlDocument, Error> {
|
||||
fn section_mut<'a>(doc: &'a mut KdlDocument, parts: &[&str], parent: &str) -> Result<&'a mut KdlDocument, Error> {
|
||||
if parts.is_empty() {
|
||||
return Ok(doc);
|
||||
}
|
||||
@@ -500,9 +448,7 @@ fn set_in(doc: &mut KdlDocument, parts: &[&str], parent: &str, value: Value) ->
|
||||
}
|
||||
}
|
||||
|
||||
if key == "autostart"
|
||||
&& rest[0] == "command"
|
||||
&& node.children().is_none_or(|d| d.get("command").is_none())
|
||||
if key == "autostart" && rest[0] == "command" && node.children().is_none_or(|d| d.get("command").is_none())
|
||||
{
|
||||
node.entries_mut().retain(|e| e.name().is_some());
|
||||
for value in value
|
||||
@@ -613,15 +559,9 @@ fn value_node(key: &str, value: &Value, parent: &str) -> Result<KdlNode, Error>
|
||||
{
|
||||
node.entries_mut().push(KdlEntry::new(kdl_value(value)?));
|
||||
}
|
||||
} else if is_records(key, parent)
|
||||
&& map.len() <= 4
|
||||
&& !value.is_array()
|
||||
&& !value.is_object()
|
||||
{
|
||||
node.entries_mut().push(KdlEntry::new_prop(
|
||||
name.replace('_', "-"),
|
||||
kdl_value(value)?,
|
||||
));
|
||||
} else if is_records(key, parent) && map.len() <= 4 && !value.is_array() && !value.is_object() {
|
||||
node.entries_mut()
|
||||
.push(KdlEntry::new_prop(name.replace('_', "-"), kdl_value(value)?));
|
||||
} else {
|
||||
append(&mut child, name, value, key)?;
|
||||
}
|
||||
@@ -643,10 +583,7 @@ fn value_node(key: &str, value: &Value, parent: &str) -> Result<KdlNode, Error>
|
||||
|
||||
fn append(doc: &mut KdlDocument, key: &str, value: &Value, parent: &str) -> Result<(), Error> {
|
||||
if is_records(key, parent) {
|
||||
for record in value
|
||||
.as_array()
|
||||
.ok_or_else(|| Error("Expected records".into()))?
|
||||
{
|
||||
for record in value.as_array().ok_or_else(|| Error("Expected records".into()))? {
|
||||
doc.nodes_mut().push(value_node(key, record, parent)?);
|
||||
}
|
||||
} else {
|
||||
@@ -677,10 +614,7 @@ desktop-widgets {
|
||||
}
|
||||
"#;
|
||||
let mut doc = Document::parse(source).unwrap();
|
||||
assert_eq!(
|
||||
doc.get("commands.my_command").unwrap(),
|
||||
&serde_json::json!(["program"])
|
||||
);
|
||||
assert_eq!(doc.get("commands.my_command").unwrap(), &serde_json::json!(["program"]));
|
||||
assert_eq!(
|
||||
doc.get("desktop_widgets.clock.outputs").unwrap(),
|
||||
&serde_json::json!(["DP-1"])
|
||||
@@ -691,8 +625,7 @@ desktop-widgets {
|
||||
);
|
||||
doc.set("commands.new_command", serde_json::json!(["two", "words"]))
|
||||
.unwrap();
|
||||
doc.set("desktop_widgets.clock.outputs", serde_json::json!([]))
|
||||
.unwrap();
|
||||
doc.set("desktop_widgets.clock.outputs", serde_json::json!([])).unwrap();
|
||||
let reparsed = Document::parse(&doc.to_string()).unwrap();
|
||||
assert_eq!(
|
||||
reparsed.get("commands.new_command").unwrap(),
|
||||
@@ -706,8 +639,7 @@ desktop-widgets {
|
||||
|
||||
#[test]
|
||||
fn inline_comments_and_failed_edits_leave_document_intact() {
|
||||
let mut doc =
|
||||
Document::parse("// header\nanimations {\n speed 0.75 // keep inline\n}\n").unwrap();
|
||||
let mut doc = Document::parse("// header\nanimations {\n speed 0.75 // keep inline\n}\n").unwrap();
|
||||
doc.set("animations.speed", 0.8.into()).unwrap();
|
||||
assert!(doc.to_string().contains("// keep inline"));
|
||||
assert_eq!(
|
||||
@@ -750,15 +682,11 @@ autostart "program" "two words" enabled=#false
|
||||
assert_eq!(doc.get("bindings.0.action").unwrap(), "toggle-overview");
|
||||
doc.set("bindings.0.action", "none".into()).unwrap();
|
||||
doc.set("input.repeat_rate", 25.into()).unwrap();
|
||||
doc.set("output_profiles.0.outputs.0.scale", 2.0.into())
|
||||
.unwrap();
|
||||
doc.set("output_profiles.0.outputs.0.scale", 2.0.into()).unwrap();
|
||||
let reparsed = Document::parse(&doc.to_string()).unwrap();
|
||||
assert_eq!(reparsed.get("bindings.0.action").unwrap(), "none");
|
||||
assert_eq!(reparsed.get("input.repeat_rate").unwrap(), 25);
|
||||
assert_eq!(
|
||||
reparsed.get("output_profiles.0.outputs.0.scale").unwrap(),
|
||||
2.0
|
||||
);
|
||||
assert_eq!(reparsed.get("output_profiles.0.outputs.0.scale").unwrap(), 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -786,28 +714,17 @@ desktop-widgets {
|
||||
}
|
||||
"#;
|
||||
let mut doc = Document::parse(source).unwrap();
|
||||
doc.set("desktop_widgets.notes.0.text", "one\ntwo".into())
|
||||
.unwrap();
|
||||
doc.set("desktop_widgets.notes.0.text", "one\ntwo".into()).unwrap();
|
||||
assert!(doc.to_string().contains("// personal comment"));
|
||||
let reparsed = Document::parse(&doc.to_string()).unwrap();
|
||||
assert_eq!(
|
||||
reparsed.get("commands.screenshot-full").unwrap(),
|
||||
&serde_json::json!(["ferese-screenshot", "--full"])
|
||||
);
|
||||
assert_eq!(
|
||||
reparsed.get("desktop_widgets.notes.0.text").unwrap(),
|
||||
"one\ntwo"
|
||||
);
|
||||
assert_eq!(reparsed.get("desktop_widgets.notes.0.text").unwrap(), "one\ntwo");
|
||||
doc.add("desktop_widgets.notes", vec![("id".into(), "b".into())])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
doc.get("desktop_widgets.notes")
|
||||
.unwrap()
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(doc.get("desktop_widgets.notes").unwrap().as_array().unwrap().len(), 2);
|
||||
doc.remove("desktop_widgets.notes", 0).unwrap();
|
||||
assert_eq!(doc.get("desktop_widgets.notes.0.id").unwrap(), "b");
|
||||
}
|
||||
|
||||
@@ -81,9 +81,7 @@ impl Default for StickyNote {
|
||||
|
||||
impl StickyNote {
|
||||
pub fn on_output(&self, name: Option<&str>) -> bool {
|
||||
self.enabled
|
||||
&& (self.outputs.is_empty()
|
||||
|| name.is_some_and(|name| self.outputs.iter().any(|s| s == name)))
|
||||
self.enabled && (self.outputs.is_empty() || name.is_some_and(|name| self.outputs.iter().any(|s| s == name)))
|
||||
}
|
||||
/// Text/style changes repaint in place, never remap the desktop card.
|
||||
pub fn same_surface(&self, other: &Self) -> bool {
|
||||
@@ -99,12 +97,7 @@ impl StickyNote {
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
let invalid = |field: &str| {
|
||||
Err(format!(
|
||||
"desktop_widgets.notes[{}].{field} is invalid",
|
||||
self.id
|
||||
))
|
||||
};
|
||||
let invalid = |field: &str| Err(format!("desktop_widgets.notes[{}].{field} is invalid", self.id));
|
||||
if self.id.is_empty()
|
||||
|| self.id.len() > 64
|
||||
|| !self
|
||||
@@ -138,9 +131,9 @@ impl StickyNote {
|
||||
for color in [&self.color, &self.background].into_iter().flatten() {
|
||||
if !color.is_empty()
|
||||
&& color != "theme"
|
||||
&& !color.strip_prefix('#').is_some_and(|hex| {
|
||||
matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit())
|
||||
})
|
||||
&& !color
|
||||
.strip_prefix('#')
|
||||
.is_some_and(|hex| matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit()))
|
||||
{
|
||||
return invalid("color/background");
|
||||
}
|
||||
@@ -253,8 +246,7 @@ impl Default for Clock {
|
||||
impl Clock {
|
||||
pub fn on_output(&self, name: Option<&str>) -> bool {
|
||||
self.enabled
|
||||
&& (self.outputs.is_empty()
|
||||
|| name.is_some_and(|name| self.outputs.iter().any(|output| output == name)))
|
||||
&& (self.outputs.is_empty() || name.is_some_and(|name| self.outputs.iter().any(|output| output == name)))
|
||||
}
|
||||
|
||||
pub fn labels(&self, now: &jiff::Zoned) -> Result<(String, String), String> {
|
||||
@@ -264,10 +256,8 @@ impl Clock {
|
||||
}
|
||||
_ => now.clone(),
|
||||
};
|
||||
let time =
|
||||
jiff::fmt::strtime::format(&self.time_format, &zoned).map_err(|e| e.to_string())?;
|
||||
let date =
|
||||
jiff::fmt::strtime::format(&self.date_format, &zoned).map_err(|e| e.to_string())?;
|
||||
let time = jiff::fmt::strtime::format(&self.time_format, &zoned).map_err(|e| e.to_string())?;
|
||||
let date = jiff::fmt::strtime::format(&self.date_format, &zoned).map_err(|e| e.to_string())?;
|
||||
Ok(if self.lowercase {
|
||||
(time.to_lowercase(), date.to_lowercase())
|
||||
} else {
|
||||
@@ -295,16 +285,14 @@ impl Clock {
|
||||
return invalid(name);
|
||||
}
|
||||
}
|
||||
for color in [&self.color, &self.date_color, &self.background]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
for color in [&self.color, &self.date_color, &self.background].into_iter().flatten() {
|
||||
if color.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !color.strip_prefix('#').is_some_and(|hex| {
|
||||
matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit())
|
||||
}) {
|
||||
if !color
|
||||
.strip_prefix('#')
|
||||
.is_some_and(|hex| matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit()))
|
||||
{
|
||||
return invalid("color");
|
||||
}
|
||||
}
|
||||
@@ -414,9 +402,6 @@ mod tests {
|
||||
date_format: "%-d %b".into(),
|
||||
..Clock::default()
|
||||
};
|
||||
assert_eq!(
|
||||
clock.labels(&now).unwrap(),
|
||||
("10:05 pm".into(), "26 sep".into())
|
||||
);
|
||||
assert_eq!(clock.labels(&now).unwrap(), ("10:05 pm".into(), "26 sep".into()));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,6 @@ mod workspace;
|
||||
|
||||
pub use output::{OutputError, OutputGeometry, OutputId, OutputWorkspaceMap, WorkspaceSwitch};
|
||||
pub use workspace::{
|
||||
LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout,
|
||||
WorkspaceSet, workspace_order_key,
|
||||
LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout, WorkspaceSet,
|
||||
workspace_order_key,
|
||||
};
|
||||
@@ -1,8 +1,6 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
error::Error,
|
||||
fmt,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
use crate::WorkspaceId;
|
||||
|
||||
@@ -19,12 +17,7 @@ pub struct OutputGeometry {
|
||||
|
||||
impl OutputGeometry {
|
||||
pub const fn new(x: i32, y: i32, width: i32, height: i32) -> Self {
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
Self { x, y, width, height }
|
||||
}
|
||||
|
||||
fn center(self) -> (i64, i64) {
|
||||
@@ -39,10 +32,7 @@ impl OutputGeometry {
|
||||
pub enum OutputError {
|
||||
AlreadyConnected(OutputId),
|
||||
UnknownOutput(OutputId),
|
||||
WorkspaceAlreadyAssigned {
|
||||
workspace: WorkspaceId,
|
||||
output: OutputId,
|
||||
},
|
||||
WorkspaceAlreadyAssigned { workspace: WorkspaceId, output: OutputId },
|
||||
}
|
||||
|
||||
impl fmt::Display for OutputError {
|
||||
@@ -51,10 +41,7 @@ impl fmt::Display for OutputError {
|
||||
Self::AlreadyConnected(output) => write!(formatter, "output {output:?} is connected"),
|
||||
Self::UnknownOutput(output) => write!(formatter, "unknown output {output:?}"),
|
||||
Self::WorkspaceAlreadyAssigned { workspace, output } => {
|
||||
write!(
|
||||
formatter,
|
||||
"workspace {workspace:?} is assigned to {output:?}"
|
||||
)
|
||||
write!(formatter, "workspace {workspace:?} is assigned to {output:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,11 +98,7 @@ impl OutputWorkspaceMap {
|
||||
}
|
||||
|
||||
pub fn forget_workspace(&mut self, workspace: WorkspaceId) -> bool {
|
||||
if self
|
||||
.outputs
|
||||
.values()
|
||||
.any(|output| output.active == workspace)
|
||||
{
|
||||
if self.outputs.values().any(|output| output.active == workspace) {
|
||||
return false;
|
||||
}
|
||||
if let Some(owner) = self.assignments.remove(&workspace)
|
||||
@@ -125,9 +108,7 @@ impl OutputWorkspaceMap {
|
||||
}
|
||||
self.revisions.remove(&workspace);
|
||||
self.evacuations.retain(|_, record| {
|
||||
record
|
||||
.workspaces
|
||||
.retain(|entry| entry.workspace != workspace);
|
||||
record.workspaces.retain(|entry| entry.workspace != workspace);
|
||||
if record.active == workspace {
|
||||
if let Some(entry) = record.workspaces.first() {
|
||||
record.active = entry.workspace;
|
||||
@@ -165,8 +146,7 @@ impl OutputWorkspaceMap {
|
||||
let mut reclaimed = Vec::new();
|
||||
if let Some(record) = &record {
|
||||
for evacuated in &record.workspaces {
|
||||
let assignment_matches =
|
||||
self.assignments.get(&evacuated.workspace).copied() == evacuated.target;
|
||||
let assignment_matches = self.assignments.get(&evacuated.workspace).copied() == evacuated.target;
|
||||
let revision_matches = self.revision(evacuated.workspace) == evacuated.revision;
|
||||
|
||||
if assignment_matches && revision_matches {
|
||||
@@ -193,12 +173,7 @@ impl OutputWorkspaceMap {
|
||||
let active = record
|
||||
.as_ref()
|
||||
.and_then(|record| reclaimed.contains(&record.active).then_some(record.active))
|
||||
.or_else(|| {
|
||||
reclaimed
|
||||
.iter()
|
||||
.copied()
|
||||
.min_by_key(|workspace| workspace.0)
|
||||
});
|
||||
.or_else(|| reclaimed.iter().copied().min_by_key(|workspace| workspace.0));
|
||||
let active = match active {
|
||||
Some(active) => active,
|
||||
None => {
|
||||
@@ -229,10 +204,7 @@ impl OutputWorkspaceMap {
|
||||
}
|
||||
|
||||
pub fn disconnect(&mut self, output: OutputId) -> Result<Option<OutputId>, OutputError> {
|
||||
let removed = self
|
||||
.outputs
|
||||
.remove(&output)
|
||||
.ok_or(OutputError::UnknownOutput(output))?;
|
||||
let removed = self.outputs.remove(&output).ok_or(OutputError::UnknownOutput(output))?;
|
||||
let target = self.migration_target(output, removed.geometry);
|
||||
let removed_had_focus = self.focused == Some(output);
|
||||
let mut evacuated = removed.workspaces.into_iter().collect::<Vec<_>>();
|
||||
@@ -309,10 +281,7 @@ impl OutputWorkspaceMap {
|
||||
}
|
||||
|
||||
let reassigned = self.assignments.insert(workspace, output) != Some(output);
|
||||
let state = self
|
||||
.outputs
|
||||
.get_mut(&output)
|
||||
.expect("output was checked above");
|
||||
let state = self.outputs.get_mut(&output).expect("output was checked above");
|
||||
state.workspaces.insert(workspace);
|
||||
state.active = workspace;
|
||||
self.focused = Some(output);
|
||||
@@ -324,11 +293,7 @@ impl OutputWorkspaceMap {
|
||||
Ok(WorkspaceSwitch::Activated(output))
|
||||
}
|
||||
|
||||
pub fn assign_workspace(
|
||||
&mut self,
|
||||
output: OutputId,
|
||||
workspace: WorkspaceId,
|
||||
) -> Result<OutputId, OutputError> {
|
||||
pub fn assign_workspace(&mut self, output: OutputId, workspace: WorkspaceId) -> Result<OutputId, OutputError> {
|
||||
if !self.outputs.contains_key(&output) {
|
||||
return Err(OutputError::UnknownOutput(output));
|
||||
}
|
||||
@@ -348,11 +313,7 @@ impl OutputWorkspaceMap {
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn migration_target(
|
||||
&self,
|
||||
removed: OutputId,
|
||||
removed_geometry: OutputGeometry,
|
||||
) -> Option<OutputId> {
|
||||
fn migration_target(&self, removed: OutputId, removed_geometry: OutputGeometry) -> Option<OutputId> {
|
||||
if let Some(focused) = self.focused
|
||||
&& focused != removed
|
||||
&& self.outputs.contains_key(&focused)
|
||||
@@ -382,10 +343,7 @@ impl OutputWorkspaceMap {
|
||||
}
|
||||
|
||||
fn validate(&self) -> bool {
|
||||
if self
|
||||
.focused
|
||||
.is_some_and(|focused| !self.outputs.contains_key(&focused))
|
||||
{
|
||||
if self.focused.is_some_and(|focused| !self.outputs.contains_key(&focused)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -423,9 +381,7 @@ mod tests {
|
||||
assert!(outputs.forget_workspace(first));
|
||||
assert_eq!(outputs.output_for_workspace(first), None);
|
||||
outputs.disconnect(display).unwrap();
|
||||
outputs
|
||||
.connect(display, geometry, super::WorkspaceId(3))
|
||||
.unwrap();
|
||||
outputs.connect(display, geometry, super::WorkspaceId(3)).unwrap();
|
||||
assert_eq!(outputs.active_workspace(display), Some(second));
|
||||
assert_eq!(outputs.output_for_workspace(first), None);
|
||||
assert!(outputs.validate());
|
||||
@@ -462,47 +418,28 @@ mod tests {
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)),
|
||||
Ok(WorkspaceId(2))
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(1)),
|
||||
Some(OutputId(1))
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(OutputId(2))
|
||||
);
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(1)));
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removal_prefers_a_remaining_focused_output_without_replacing_its_workspace() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
|
||||
outputs.focus_output(OutputId(1)).unwrap();
|
||||
|
||||
assert_eq!(outputs.disconnect(OutputId(2)), Ok(Some(OutputId(1))));
|
||||
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(OutputId(1))
|
||||
);
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_the_focused_output_activates_its_workspace_on_the_nearest_output() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(3), geometry(3_840), WorkspaceId(3))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
|
||||
outputs.connect(OutputId(3), geometry(3_840), WorkspaceId(3)).unwrap();
|
||||
outputs.focus_output(OutputId(2)).unwrap();
|
||||
|
||||
assert_eq!(outputs.disconnect(OutputId(2)), Ok(Some(OutputId(1))));
|
||||
@@ -513,12 +450,8 @@ mod tests {
|
||||
#[test]
|
||||
fn reconnect_reclaims_automatically_evacuated_workspaces() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
|
||||
outputs.focus_output(OutputId(1)).unwrap();
|
||||
outputs.disconnect(OutputId(2)).unwrap();
|
||||
|
||||
@@ -526,29 +459,20 @@ mod tests {
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)),
|
||||
Ok(WorkspaceId(2))
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(OutputId(2))
|
||||
);
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
|
||||
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconnect_restores_the_migration_targets_remaining_workspace() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
|
||||
outputs.focus_output(OutputId(2)).unwrap();
|
||||
outputs.disconnect(OutputId(2)).unwrap();
|
||||
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(2)));
|
||||
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1_920), WorkspaceId(3))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)).unwrap();
|
||||
|
||||
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
|
||||
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2)));
|
||||
@@ -557,37 +481,24 @@ mod tests {
|
||||
#[test]
|
||||
fn using_an_evacuated_workspace_does_not_prevent_automatic_return() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
|
||||
outputs.focus_output(OutputId(1)).unwrap();
|
||||
outputs.disconnect(OutputId(2)).unwrap();
|
||||
outputs
|
||||
.switch_workspace(OutputId(1), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs.switch_workspace(OutputId(1), WorkspaceId(2)).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)),
|
||||
Ok(WorkspaceId(2))
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(OutputId(2))
|
||||
);
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_lid_and_monitor_handoffs_restore_both_workspace_owners() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1920), WorkspaceId(2)).unwrap();
|
||||
for removed in [OutputId(1), OutputId(2), OutputId(1), OutputId(2)] {
|
||||
let remaining = if removed == OutputId(1) {
|
||||
OutputId(2)
|
||||
@@ -596,14 +507,8 @@ mod tests {
|
||||
};
|
||||
outputs.focus_output(removed).unwrap();
|
||||
outputs.disconnect(removed).unwrap();
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(1)),
|
||||
Some(remaining)
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(remaining)
|
||||
);
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(remaining));
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(remaining));
|
||||
// Switching between handed-off workspaces is normal use, not an
|
||||
// explicit change of their home output.
|
||||
outputs.switch_workspace(remaining, WorkspaceId(1)).unwrap();
|
||||
@@ -615,14 +520,8 @@ mod tests {
|
||||
WorkspaceId(3),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(1)),
|
||||
Some(OutputId(1))
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(OutputId(2))
|
||||
);
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(1)));
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
|
||||
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
|
||||
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2)));
|
||||
}
|
||||
@@ -631,12 +530,8 @@ mod tests {
|
||||
#[test]
|
||||
fn selecting_a_workspace_owned_by_another_output_focuses_that_output() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
outputs.switch_workspace(OutputId(1), WorkspaceId(2)),
|
||||
@@ -648,18 +543,10 @@ mod tests {
|
||||
#[test]
|
||||
fn assigning_an_unused_workspace_does_not_change_the_active_workspace() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
outputs.assign_workspace(OutputId(1), WorkspaceId(2)),
|
||||
Ok(OutputId(1))
|
||||
);
|
||||
assert_eq!(outputs.assign_workspace(OutputId(1), WorkspaceId(2)), Ok(OutputId(1)));
|
||||
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(OutputId(1))
|
||||
);
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(1)));
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
use ferese_layout::{
|
||||
Axis, ColumnWidth, Direction, GapConfig, LayoutError, LayoutResult, LayoutTree, Rect,
|
||||
ScrollingLayout, SizeConstraints, ViewportFocusStrategy, WindowId,
|
||||
Axis, ColumnWidth, Direction, GapConfig, LayoutError, LayoutResult, LayoutTree, Rect, ScrollingLayout,
|
||||
SizeConstraints, ViewportFocusStrategy, WindowId,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -44,11 +44,7 @@ impl Default for WorkspaceLayout {
|
||||
}
|
||||
|
||||
impl WorkspaceLayout {
|
||||
pub fn new(
|
||||
mode: LayoutMode,
|
||||
default_column_width: ColumnWidth,
|
||||
focus_strategy: ViewportFocusStrategy,
|
||||
) -> Self {
|
||||
pub fn new(mode: LayoutMode, default_column_width: ColumnWidth, focus_strategy: ViewportFocusStrategy) -> Self {
|
||||
match mode {
|
||||
LayoutMode::Scrolling => {
|
||||
let mut layout = ScrollingLayout::with_default_width(default_column_width);
|
||||
@@ -161,17 +157,11 @@ impl WorkspaceLayout {
|
||||
constraints,
|
||||
focused.filter(|window| layout.contains(*window)),
|
||||
),
|
||||
Self::Tree(layout) => {
|
||||
layout.geometry_with_constraints(bounds, gaps, constraints, focused)
|
||||
}
|
||||
Self::Tree(layout) => layout.geometry_with_constraints(bounds, gaps, constraints, focused),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn automatic_axis(
|
||||
&self,
|
||||
focused: Option<WindowId>,
|
||||
bounds: Rect,
|
||||
) -> Result<Axis, LayoutError> {
|
||||
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),
|
||||
@@ -190,24 +180,14 @@ impl WorkspaceLayout {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_window(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
direction: Direction,
|
||||
bounds: Rect,
|
||||
) -> Result<bool, LayoutError> {
|
||||
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> {
|
||||
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),
|
||||
@@ -229,11 +209,7 @@ impl WorkspaceLayout {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle_column_width(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
presets: &[ColumnWidth],
|
||||
) -> Result<bool, LayoutError> {
|
||||
pub fn cycle_column_width(&mut self, window: WindowId, presets: &[ColumnWidth]) -> Result<bool, LayoutError> {
|
||||
match self {
|
||||
Self::Scrolling(layout) => layout.cycle_column_width(window, presets),
|
||||
Self::Tree(_) => Ok(false),
|
||||
@@ -280,11 +256,7 @@ impl WorkspaceLayout {
|
||||
|
||||
for column in columns {
|
||||
for (row, window) in column.windows.into_iter().enumerate() {
|
||||
let axis = if row == 0 {
|
||||
Axis::Horizontal
|
||||
} else {
|
||||
Axis::Vertical
|
||||
};
|
||||
let axis = if row == 0 { Axis::Horizontal } else { Axis::Vertical };
|
||||
tree.insert(window, previous, axis, 0.5)?;
|
||||
previous = Some(window);
|
||||
}
|
||||
@@ -444,12 +416,7 @@ impl WorkspaceSet {
|
||||
}
|
||||
|
||||
/// Update policy without moving windows or replacing existing column widths.
|
||||
pub fn reconfigure_defaults(
|
||||
&mut self,
|
||||
mode: LayoutMode,
|
||||
width: ColumnWidth,
|
||||
strategy: ViewportFocusStrategy,
|
||||
) {
|
||||
pub fn reconfigure_defaults(&mut self, mode: LayoutMode, width: ColumnWidth, strategy: ViewportFocusStrategy) {
|
||||
self.default_layout_mode = mode;
|
||||
self.default_column_width = width;
|
||||
self.scrolling_focus_strategy = strategy;
|
||||
@@ -470,11 +437,7 @@ impl WorkspaceSet {
|
||||
let workspace = Workspace {
|
||||
id: active,
|
||||
name: "1".to_owned(),
|
||||
layout: WorkspaceLayout::new(
|
||||
default_layout_mode,
|
||||
default_column_width,
|
||||
scrolling_focus_strategy,
|
||||
),
|
||||
layout: WorkspaceLayout::new(default_layout_mode, default_column_width, scrolling_focus_strategy),
|
||||
floating: Vec::new(),
|
||||
last_focused: None,
|
||||
fullscreen: None,
|
||||
@@ -520,12 +483,8 @@ impl WorkspaceSet {
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
let focused = self.active().last_focused;
|
||||
let visible_floating = focused.filter(|window| {
|
||||
matches!(
|
||||
self.placement(*window),
|
||||
Some(WindowPlacement::Floating { .. })
|
||||
)
|
||||
});
|
||||
let visible_floating =
|
||||
focused.filter(|window| matches!(self.placement(*window), Some(WindowPlacement::Floating { .. })));
|
||||
Ok(visible_floating.or(self.active().fullscreen).or(focused))
|
||||
}
|
||||
|
||||
@@ -543,8 +502,7 @@ impl WorkspaceSet {
|
||||
|
||||
pub fn ordered(&self) -> Vec<&Workspace> {
|
||||
let mut workspaces = self.iter().collect::<Vec<_>>();
|
||||
workspaces
|
||||
.sort_unstable_by_key(|workspace| workspace_order_key(&workspace.name, workspace.id));
|
||||
workspaces.sort_unstable_by_key(|workspace| workspace_order_key(&workspace.name, workspace.id));
|
||||
workspaces
|
||||
}
|
||||
|
||||
@@ -552,20 +510,12 @@ impl WorkspaceSet {
|
||||
let mut empty = self
|
||||
.workspaces
|
||||
.values()
|
||||
.filter(|workspace| {
|
||||
workspace.layout.window_ids().next().is_none() && workspace.floating.is_empty()
|
||||
})
|
||||
.filter(|workspace| workspace.layout.window_ids().next().is_none() && workspace.floating.is_empty())
|
||||
.map(|workspace| workspace.id)
|
||||
.collect::<Vec<_>>();
|
||||
empty.sort_by_key(|id| id.0);
|
||||
let visible_empty = empty
|
||||
.iter()
|
||||
.any(|id| *id == self.active || protected.contains(id));
|
||||
let spare = if visible_empty {
|
||||
None
|
||||
} else {
|
||||
empty.first().copied()
|
||||
};
|
||||
let visible_empty = empty.iter().any(|id| *id == self.active || protected.contains(id));
|
||||
let spare = if visible_empty { None } else { empty.first().copied() };
|
||||
let mut removed = Vec::new();
|
||||
for id in empty {
|
||||
if id == self.active || protected.contains(&id) || Some(id) == spare {
|
||||
@@ -588,32 +538,20 @@ impl WorkspaceSet {
|
||||
self.placements.get(&window).copied()
|
||||
}
|
||||
|
||||
pub fn set_active_layout_mode(
|
||||
&mut self,
|
||||
mode: LayoutMode,
|
||||
bounds: Rect,
|
||||
) -> Result<bool, WorkspaceError> {
|
||||
pub fn set_active_layout_mode(&mut self, mode: LayoutMode, bounds: Rect) -> Result<bool, WorkspaceError> {
|
||||
let default_column_width = self.default_column_width;
|
||||
let focus_strategy = self.scrolling_focus_strategy;
|
||||
let workspace = self.active_mut();
|
||||
let focused = tiled_focus(workspace);
|
||||
let changed = workspace.layout.set_mode(
|
||||
mode,
|
||||
bounds,
|
||||
focused,
|
||||
default_column_width,
|
||||
focus_strategy,
|
||||
)?;
|
||||
let changed = workspace
|
||||
.layout
|
||||
.set_mode(mode, bounds, focused, default_column_width, focus_strategy)?;
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
pub fn set_floating_rect(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
rect: Rect,
|
||||
) -> Result<(), WorkspaceError> {
|
||||
pub fn set_floating_rect(&mut self, window: WindowId, rect: Rect) -> Result<(), WorkspaceError> {
|
||||
let placement = self
|
||||
.placements
|
||||
.get_mut(&window)
|
||||
@@ -639,35 +577,23 @@ impl WorkspaceSet {
|
||||
self.focus_window_with_reveal(window, false)
|
||||
}
|
||||
|
||||
fn focus_window_with_reveal(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
reveal: bool,
|
||||
) -> Result<(), WorkspaceError> {
|
||||
fn focus_window_with_reveal(&mut self, window: WindowId, reveal: bool) -> Result<(), WorkspaceError> {
|
||||
if self.workspace_for_window(window) != Some(self.active) {
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"focused window is not on the active workspace",
|
||||
));
|
||||
}
|
||||
if self.active().fullscreen.is_some_and(|fullscreen| {
|
||||
fullscreen != window
|
||||
&& !matches!(
|
||||
self.placement(window),
|
||||
Some(WindowPlacement::Floating { .. })
|
||||
)
|
||||
fullscreen != window && !matches!(self.placement(window), Some(WindowPlacement::Floating { .. }))
|
||||
}) {
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"focused window is hidden by fullscreen",
|
||||
));
|
||||
return Err(WorkspaceError::InvalidState("focused window is hidden by fullscreen"));
|
||||
}
|
||||
|
||||
if self.placement(window) == Some(WindowPlacement::Tiled) {
|
||||
if reveal {
|
||||
self.active_mut().layout.activate_window(window)?;
|
||||
} else {
|
||||
self.active_mut()
|
||||
.layout
|
||||
.activate_window_without_reveal(window)?;
|
||||
self.active_mut().layout.activate_window_without_reveal(window)?;
|
||||
}
|
||||
}
|
||||
self.active_mut().last_focused = Some(window);
|
||||
@@ -676,11 +602,7 @@ impl WorkspaceSet {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stack_window(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
target: WindowId,
|
||||
) -> Result<(), WorkspaceError> {
|
||||
pub fn stack_window(&mut self, window: WindowId, target: WindowId) -> Result<(), WorkspaceError> {
|
||||
let workspace = self
|
||||
.workspace_for_window(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
@@ -721,11 +643,7 @@ impl WorkspaceSet {
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
pub fn cycle_column_width(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
presets: &[ColumnWidth],
|
||||
) -> Result<bool, WorkspaceError> {
|
||||
pub fn cycle_column_width(&mut self, window: WindowId, presets: &[ColumnWidth]) -> Result<bool, WorkspaceError> {
|
||||
if self.workspace_for_window(window) != Some(self.active)
|
||||
|| self.placement(window) != Some(WindowPlacement::Tiled)
|
||||
{
|
||||
@@ -734,10 +652,7 @@ impl WorkspaceSet {
|
||||
));
|
||||
}
|
||||
|
||||
let changed = self
|
||||
.active_mut()
|
||||
.layout
|
||||
.cycle_column_width(window, presets)?;
|
||||
let changed = self.active_mut().layout.cycle_column_width(window, presets)?;
|
||||
debug_assert!(self.validate().is_ok());
|
||||
Ok(changed)
|
||||
}
|
||||
@@ -804,12 +719,7 @@ impl WorkspaceSet {
|
||||
self.activate(workspace)
|
||||
}
|
||||
|
||||
pub fn insert_window(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
axis: Axis,
|
||||
ratio: f64,
|
||||
) -> Result<(), WorkspaceError> {
|
||||
pub fn insert_window(&mut self, window: WindowId, axis: Axis, ratio: f64) -> Result<(), WorkspaceError> {
|
||||
if self.window_workspaces.contains_key(&window) {
|
||||
return Err(WorkspaceError::DuplicateWindow(window));
|
||||
}
|
||||
@@ -1004,11 +914,7 @@ impl WorkspaceSet {
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
pub fn set_fullscreen(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
enabled: bool,
|
||||
) -> Result<bool, WorkspaceError> {
|
||||
pub fn set_fullscreen(&mut self, window: WindowId, enabled: bool) -> Result<bool, WorkspaceError> {
|
||||
let workspace_id = self
|
||||
.workspace_for_window(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
@@ -1038,18 +944,14 @@ impl WorkspaceSet {
|
||||
return Err(WorkspaceError::InvalidState("active workspace is missing"));
|
||||
}
|
||||
if self.names.len() != self.workspaces.len() {
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"workspace name index is inconsistent",
|
||||
));
|
||||
return Err(WorkspaceError::InvalidState("workspace name index is inconsistent"));
|
||||
}
|
||||
|
||||
let mut seen_windows = HashSet::new();
|
||||
|
||||
for (id, workspace) in &self.workspaces {
|
||||
if workspace.id != *id || self.names.get(&workspace.name) != Some(id) {
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"workspace index is inconsistent",
|
||||
));
|
||||
return Err(WorkspaceError::InvalidState("workspace index is inconsistent"));
|
||||
}
|
||||
|
||||
workspace.layout.validate()?;
|
||||
@@ -1059,9 +961,7 @@ impl WorkspaceSet {
|
||||
|| self.window_workspaces.get(&window) != Some(id)
|
||||
|| self.placements.get(&window) != Some(&WindowPlacement::Tiled)
|
||||
{
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"window ownership is inconsistent",
|
||||
));
|
||||
return Err(WorkspaceError::InvalidState("window ownership is inconsistent"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1071,10 +971,7 @@ impl WorkspaceSet {
|
||||
if !seen_floating.insert(*window)
|
||||
|| !seen_windows.insert(*window)
|
||||
|| self.window_workspaces.get(window) != Some(id)
|
||||
|| !matches!(
|
||||
self.placements.get(window),
|
||||
Some(WindowPlacement::Floating { .. })
|
||||
)
|
||||
|| !matches!(self.placements.get(window), Some(WindowPlacement::Floating { .. }))
|
||||
{
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"floating window ownership is inconsistent",
|
||||
@@ -1086,9 +983,7 @@ impl WorkspaceSet {
|
||||
.last_focused
|
||||
.is_some_and(|window| self.window_workspaces.get(&window) != Some(id))
|
||||
{
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"workspace focus is inconsistent",
|
||||
));
|
||||
return Err(WorkspaceError::InvalidState("workspace focus is inconsistent"));
|
||||
}
|
||||
|
||||
if workspace
|
||||
@@ -1101,12 +996,8 @@ impl WorkspaceSet {
|
||||
}
|
||||
}
|
||||
|
||||
if seen_windows.len() != self.window_workspaces.len()
|
||||
|| self.placements.len() != self.window_workspaces.len()
|
||||
{
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"window index retains stale entries",
|
||||
));
|
||||
if seen_windows.len() != self.window_workspaces.len() || self.placements.len() != self.window_workspaces.len() {
|
||||
return Err(WorkspaceError::InvalidState("window index retains stale entries"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1114,13 +1005,10 @@ impl WorkspaceSet {
|
||||
}
|
||||
|
||||
fn first_window(workspace: &Workspace) -> Option<WindowId> {
|
||||
workspace.layout.preferred_window().or_else(|| {
|
||||
workspace
|
||||
.floating
|
||||
.iter()
|
||||
.copied()
|
||||
.min_by_key(|window| window.0)
|
||||
})
|
||||
workspace
|
||||
.layout
|
||||
.preferred_window()
|
||||
.or_else(|| workspace.floating.iter().copied().min_by_key(|window| window.0))
|
||||
}
|
||||
|
||||
fn tiled_focus(workspace: &Workspace) -> Option<WindowId> {
|
||||
@@ -1151,10 +1039,7 @@ mod tests {
|
||||
let original_first = set.active_id();
|
||||
let second = set.ensure_numeric(2).unwrap();
|
||||
set.activate(second).unwrap();
|
||||
assert_eq!(
|
||||
set.prune_empty(&std::collections::HashSet::new()),
|
||||
vec![original_first]
|
||||
);
|
||||
assert_eq!(set.prune_empty(&std::collections::HashSet::new()), vec![original_first]);
|
||||
|
||||
let tenth = set.ensure_numeric(10).unwrap();
|
||||
let first = set.ensure_numeric(1).unwrap();
|
||||
@@ -1188,19 +1073,13 @@ mod tests {
|
||||
.unwrap();
|
||||
let spare = set.ensure_numeric(3).unwrap();
|
||||
let excess = set.ensure_numeric(4).unwrap();
|
||||
assert_eq!(
|
||||
set.prune_empty(&std::collections::HashSet::new()),
|
||||
vec![excess]
|
||||
);
|
||||
assert_eq!(set.prune_empty(&std::collections::HashSet::new()), vec![excess]);
|
||||
assert!(set.workspace(first).is_some());
|
||||
assert!(set.workspace(floating).is_some());
|
||||
assert!(set.workspace(spare).is_some());
|
||||
set.activate(spare).unwrap();
|
||||
set.remove_window(super::WindowId(1)).unwrap();
|
||||
assert_eq!(
|
||||
set.prune_empty(&std::collections::HashSet::new()),
|
||||
vec![first]
|
||||
);
|
||||
assert_eq!(set.prune_empty(&std::collections::HashSet::new()), vec![first]);
|
||||
assert!(set.validate().is_ok());
|
||||
assert_ne!(set.ensure_numeric(1).unwrap(), first);
|
||||
}
|
||||
@@ -1222,9 +1101,7 @@ mod tests {
|
||||
fn live_policy_updates_existing_layouts_and_default_widths_not_custom_widths() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
for id in 1..=3 {
|
||||
workspaces
|
||||
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
|
||||
}
|
||||
let WorkspaceLayout::Scrolling(layout) = &mut workspaces.active_mut().layout else {
|
||||
panic!()
|
||||
@@ -1263,30 +1140,18 @@ mod tests {
|
||||
#[test]
|
||||
fn reconfiguration_preserves_windows_and_current_widths_but_updates_defaults() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
let workspace = workspaces.active_id();
|
||||
workspaces.reconfigure_defaults(
|
||||
LayoutMode::Tree,
|
||||
ColumnWidth::Full,
|
||||
ViewportFocusStrategy::Minimal,
|
||||
);
|
||||
workspaces.reconfigure_defaults(LayoutMode::Tree, ColumnWidth::Full, ViewportFocusStrategy::Minimal);
|
||||
let WorkspaceLayout::Scrolling(layout) = &workspaces.active().layout else {
|
||||
panic!("existing layout must survive");
|
||||
};
|
||||
assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(0.5));
|
||||
assert_eq!(layout.default_width(), ColumnWidth::Full);
|
||||
assert_eq!(
|
||||
workspaces.workspace_for_window(WindowId(1)),
|
||||
Some(workspace)
|
||||
);
|
||||
assert_eq!(workspaces.workspace_for_window(WindowId(1)), Some(workspace));
|
||||
workspaces.switch_to_numeric(2).unwrap();
|
||||
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Tree);
|
||||
assert_eq!(
|
||||
workspaces.workspace_for_window(WindowId(1)),
|
||||
Some(workspace)
|
||||
);
|
||||
assert_eq!(workspaces.workspace_for_window(WindowId(1)), Some(workspace));
|
||||
}
|
||||
use super::*;
|
||||
|
||||
@@ -1304,14 +1169,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn configured_layout_defaults_apply_to_new_workspaces() {
|
||||
let mut workspaces = WorkspaceSet::new(
|
||||
LayoutMode::Scrolling,
|
||||
ColumnWidth::Full,
|
||||
ViewportFocusStrategy::Minimal,
|
||||
);
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
let mut workspaces =
|
||||
WorkspaceSet::new(LayoutMode::Scrolling, ColumnWidth::Full, ViewportFocusStrategy::Minimal);
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
let first = workspaces
|
||||
.active_mut()
|
||||
.layout
|
||||
@@ -1323,10 +1183,7 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
first.geometry[&WindowId(1)],
|
||||
Rect::new(4.0, 4.0, 992.0, 792.0)
|
||||
);
|
||||
assert_eq!(first.geometry[&WindowId(1)], Rect::new(4.0, 4.0, 992.0, 792.0));
|
||||
workspaces.switch_to_numeric(2).unwrap();
|
||||
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling);
|
||||
}
|
||||
@@ -1334,14 +1191,10 @@ mod tests {
|
||||
#[test]
|
||||
fn switching_restores_workspace_focus() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
assert_eq!(workspaces.switch_to_numeric(2).unwrap(), None);
|
||||
workspaces
|
||||
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
assert_eq!(workspaces.switch_to_numeric(1).unwrap(), Some(WindowId(1)));
|
||||
assert_eq!(workspaces.switch_to_numeric(2).unwrap(), Some(WindowId(2)));
|
||||
@@ -1351,12 +1204,8 @@ mod tests {
|
||||
#[test]
|
||||
fn moving_a_window_updates_membership_without_switching() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces
|
||||
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||
let first = workspaces.active_id();
|
||||
let second = workspaces
|
||||
.move_window_to_numeric(WindowId(2), 2, Axis::Horizontal, 0.5)
|
||||
@@ -1364,32 +1213,16 @@ mod tests {
|
||||
|
||||
assert_eq!(workspaces.active_id(), first);
|
||||
assert_eq!(workspaces.workspace_for_window(WindowId(2)), Some(second));
|
||||
assert!(
|
||||
!workspaces
|
||||
.workspace(first)
|
||||
.unwrap()
|
||||
.layout
|
||||
.contains(WindowId(2))
|
||||
);
|
||||
assert!(
|
||||
workspaces
|
||||
.workspace(second)
|
||||
.unwrap()
|
||||
.layout
|
||||
.contains(WindowId(2))
|
||||
);
|
||||
assert!(!workspaces.workspace(first).unwrap().layout.contains(WindowId(2)));
|
||||
assert!(workspaces.workspace(second).unwrap().layout.contains(WindowId(2)));
|
||||
assert!(workspaces.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_focus_chooses_a_deterministic_remaining_window() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces.remove_window(WindowId(1)).unwrap();
|
||||
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
||||
@@ -1400,9 +1233,7 @@ mod tests {
|
||||
fn closing_a_scrolling_column_focuses_its_adjacent_column() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
for id in 1..=4 {
|
||||
workspaces
|
||||
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
|
||||
}
|
||||
workspaces.focus_window(WindowId(3)).unwrap();
|
||||
|
||||
@@ -1414,9 +1245,7 @@ mod tests {
|
||||
#[test]
|
||||
fn focus_rejects_a_window_on_an_inactive_workspace() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces
|
||||
.move_window_to_numeric(WindowId(1), 2, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
@@ -1433,9 +1262,7 @@ mod tests {
|
||||
#[test]
|
||||
fn floating_toggle_removes_and_restores_tiled_membership() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
let rect = Rect::new(10.0, 20.0, 640.0, 480.0);
|
||||
|
||||
assert_eq!(
|
||||
@@ -1461,21 +1288,14 @@ mod tests {
|
||||
#[test]
|
||||
fn floating_geometry_updates_are_authoritative_and_validated() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
assert_eq!(
|
||||
workspaces.set_floating_rect(WindowId(1), Rect::default()),
|
||||
Err(WorkspaceError::InvalidState("window is not floating"))
|
||||
);
|
||||
|
||||
workspaces
|
||||
.toggle_floating(
|
||||
WindowId(1),
|
||||
Rect::new(10.0, 20.0, 640.0, 480.0),
|
||||
Axis::Horizontal,
|
||||
0.5,
|
||||
)
|
||||
.toggle_floating(WindowId(1), Rect::new(10.0, 20.0, 640.0, 480.0), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces
|
||||
.set_floating_rect(WindowId(1), Rect::new(30.0, 40.0, f64::NAN, -2.0))
|
||||
@@ -1497,15 +1317,9 @@ mod tests {
|
||||
ViewportFocusStrategy::Center,
|
||||
ViewportFocusStrategy::Paged,
|
||||
] {
|
||||
let mut workspaces = WorkspaceSet::new(
|
||||
LayoutMode::Scrolling,
|
||||
ColumnWidth::Proportion(0.5),
|
||||
strategy,
|
||||
);
|
||||
let mut workspaces = WorkspaceSet::new(LayoutMode::Scrolling, ColumnWidth::Proportion(0.5), strategy);
|
||||
for id in 1..=3 {
|
||||
workspaces
|
||||
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
|
||||
}
|
||||
let bounds = Rect::new(0.0, 0.0, 1000.0, 800.0);
|
||||
let gaps = GapConfig {
|
||||
@@ -1555,20 +1369,13 @@ mod tests {
|
||||
ViewportFocusStrategy::Paged,
|
||||
);
|
||||
for id in 1..=4 {
|
||||
workspaces
|
||||
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
|
||||
}
|
||||
let bounds = Rect::new(0.0, 0.0, 1000.0, 800.0);
|
||||
let before = workspaces
|
||||
.active_mut()
|
||||
.layout
|
||||
.geometry_with_constraints(
|
||||
bounds,
|
||||
GapConfig::default(),
|
||||
&HashMap::new(),
|
||||
Some(WindowId(4)),
|
||||
)
|
||||
.geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(4)))
|
||||
.unwrap();
|
||||
let scroll = workspaces.active().layout.viewport_x();
|
||||
workspaces
|
||||
@@ -1582,12 +1389,7 @@ mod tests {
|
||||
let after = workspaces
|
||||
.active_mut()
|
||||
.layout
|
||||
.geometry_with_constraints(
|
||||
bounds,
|
||||
GapConfig::default(),
|
||||
&HashMap::new(),
|
||||
Some(WindowId(5)),
|
||||
)
|
||||
.geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(5)))
|
||||
.unwrap();
|
||||
assert_eq!(before.geometry, after.geometry);
|
||||
assert_eq!(scroll, workspaces.active().layout.viewport_x());
|
||||
@@ -1598,9 +1400,7 @@ mod tests {
|
||||
#[test]
|
||||
fn floating_window_can_follow_its_parent_workspace_without_focus() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
let parent_workspace = workspaces.active_id();
|
||||
workspaces.switch_to_numeric(2).unwrap();
|
||||
workspaces
|
||||
@@ -1612,10 +1412,7 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
workspaces.workspace_for_window(WindowId(2)),
|
||||
Some(parent_workspace)
|
||||
);
|
||||
assert_eq!(workspaces.workspace_for_window(WindowId(2)), Some(parent_workspace));
|
||||
assert_eq!(
|
||||
workspaces.workspace(parent_workspace).unwrap().last_focused,
|
||||
Some(WindowId(1))
|
||||
@@ -1626,9 +1423,7 @@ mod tests {
|
||||
#[test]
|
||||
fn fullscreen_preserves_underlying_placement() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
let before = workspaces.placement(WindowId(1));
|
||||
|
||||
assert!(workspaces.toggle_fullscreen(WindowId(1)).unwrap());
|
||||
@@ -1646,13 +1441,9 @@ mod tests {
|
||||
#[test]
|
||||
fn mapping_under_fullscreen_does_not_change_focus() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces.toggle_fullscreen(WindowId(1)).unwrap();
|
||||
workspaces
|
||||
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(1)));
|
||||
assert_eq!(workspaces.active().fullscreen, Some(WindowId(1)));
|
||||
@@ -1664,17 +1455,10 @@ mod tests {
|
||||
fn floating_dialog_can_focus_above_fullscreen() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
let workspace = workspaces.active_id();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces.toggle_fullscreen(WindowId(1)).unwrap();
|
||||
workspaces
|
||||
.insert_floating_window(
|
||||
WindowId(2),
|
||||
workspace,
|
||||
Rect::new(100.0, 100.0, 500.0, 400.0),
|
||||
true,
|
||||
)
|
||||
.insert_floating_window(WindowId(2), workspace, Rect::new(100.0, 100.0, 500.0, 400.0), true)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
||||
@@ -1688,14 +1472,10 @@ mod tests {
|
||||
#[test]
|
||||
fn switching_to_fullscreen_restores_the_visible_window() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces.toggle_fullscreen(WindowId(1)).unwrap();
|
||||
workspaces.switch_to_numeric(2).unwrap();
|
||||
workspaces
|
||||
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces
|
||||
.move_window_to_numeric(WindowId(2), 1, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
@@ -1704,9 +1484,7 @@ mod tests {
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(1)));
|
||||
assert_eq!(
|
||||
workspaces.focus_window(WindowId(2)),
|
||||
Err(WorkspaceError::InvalidState(
|
||||
"focused window is hidden by fullscreen"
|
||||
))
|
||||
Err(WorkspaceError::InvalidState("focused window is hidden by fullscreen"))
|
||||
);
|
||||
assert!(workspaces.validate().is_ok());
|
||||
}
|
||||
@@ -1714,12 +1492,8 @@ mod tests {
|
||||
#[test]
|
||||
fn grouping_tiled_windows_preserves_workspace_membership() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces
|
||||
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces.stack_window(WindowId(2), WindowId(1)).unwrap();
|
||||
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
||||
@@ -1740,26 +1514,16 @@ mod tests {
|
||||
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.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!(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<_>>(),
|
||||
workspaces.active().layout.window_ids().collect::<HashSet<_>>(),
|
||||
HashSet::from([WindowId(1), WindowId(2), WindowId(3), WindowId(4)])
|
||||
);
|
||||
|
||||
@@ -1777,19 +1541,14 @@ mod tests {
|
||||
fn scrolling_column_commands_preserve_workspace_state() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
for id in 1..=3 {
|
||||
workspaces
|
||||
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
|
||||
}
|
||||
|
||||
workspaces.stack_window(WindowId(3), WindowId(2)).unwrap();
|
||||
assert!(workspaces.extract_window(WindowId(3)).unwrap());
|
||||
assert!(
|
||||
workspaces
|
||||
.cycle_column_width(
|
||||
WindowId(3),
|
||||
&[ColumnWidth::Proportion(0.5), ColumnWidth::Full],
|
||||
)
|
||||
.cycle_column_width(WindowId(3), &[ColumnWidth::Proportion(0.5), ColumnWidth::Full],)
|
||||
.unwrap()
|
||||
);
|
||||
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
|
||||
@@ -1817,9 +1576,7 @@ mod tests {
|
||||
0 if windows.len() < 48 => {
|
||||
let window = WindowId(next_window);
|
||||
next_window += 1;
|
||||
workspaces
|
||||
.insert_window(window, random_axis(random), 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(window, random_axis(random), 0.5).unwrap();
|
||||
windows.push(window);
|
||||
}
|
||||
1 if !windows.is_empty() => {
|
||||
@@ -1866,8 +1623,7 @@ mod tests {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(window) = candidates.get(random as usize % candidates.len().max(1))
|
||||
{
|
||||
if let Some(window) = candidates.get(random as usize % candidates.len().max(1)) {
|
||||
workspaces.focus_window(*window).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::io::{Read, Write};
|
||||
use std::{fmt, io};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -69,10 +69,7 @@ impl fmt::Display for FrameError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Io(error) => error.fmt(formatter),
|
||||
Self::Oversized(size) => write!(
|
||||
formatter,
|
||||
"IPC payload is {size} bytes; maximum is {MAX_PAYLOAD_SIZE}"
|
||||
),
|
||||
Self::Oversized(size) => write!(formatter, "IPC payload is {size} bytes; maximum is {MAX_PAYLOAD_SIZE}"),
|
||||
Self::InvalidJson(error) => write!(formatter, "invalid IPC JSON: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
+56
-202
@@ -54,12 +54,7 @@ pub struct Rect {
|
||||
|
||||
impl Rect {
|
||||
pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
Self { x, y, width, height }
|
||||
}
|
||||
|
||||
fn center(self) -> (f64, f64) {
|
||||
@@ -365,11 +360,7 @@ impl LayoutTree {
|
||||
Ok(geometry)
|
||||
}
|
||||
|
||||
pub fn geometry_with_gaps(
|
||||
&self,
|
||||
bounds: Rect,
|
||||
gaps: GapConfig,
|
||||
) -> Result<HashMap<WindowId, Rect>, LayoutError> {
|
||||
pub fn geometry_with_gaps(&self, bounds: Rect, gaps: GapConfig) -> Result<HashMap<WindowId, Rect>, LayoutError> {
|
||||
let mut geometry = self.geometry(bounds)?;
|
||||
apply_gaps(&mut geometry, bounds, gaps);
|
||||
|
||||
@@ -434,11 +425,7 @@ impl LayoutTree {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn automatic_axis(
|
||||
&self,
|
||||
focused: Option<WindowId>,
|
||||
bounds: Rect,
|
||||
) -> Result<Axis, LayoutError> {
|
||||
pub fn automatic_axis(&self, focused: Option<WindowId>, bounds: Rect) -> Result<Axis, LayoutError> {
|
||||
let Some(root) = self.root else {
|
||||
return Ok(Axis::Horizontal);
|
||||
};
|
||||
@@ -516,12 +503,7 @@ impl LayoutTree {
|
||||
.map(|(candidate, _, _, _)| candidate))
|
||||
}
|
||||
|
||||
pub fn move_window(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
direction: Direction,
|
||||
bounds: Rect,
|
||||
) -> Result<bool, LayoutError> {
|
||||
pub fn move_window(&mut self, window: WindowId, direction: Direction, bounds: Rect) -> Result<bool, LayoutError> {
|
||||
let Some(neighbor) = self.directional_neighbor(window, direction, bounds)? else {
|
||||
return Ok(false);
|
||||
};
|
||||
@@ -545,12 +527,7 @@ impl LayoutTree {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn resize_window(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
direction: Direction,
|
||||
amount: f64,
|
||||
) -> Result<bool, LayoutError> {
|
||||
pub fn resize_window(&mut self, window: WindowId, direction: Direction, amount: f64) -> Result<bool, LayoutError> {
|
||||
let mut child = self
|
||||
.windows
|
||||
.get(&window)
|
||||
@@ -651,9 +628,7 @@ impl LayoutTree {
|
||||
match self.nodes.get(&node) {
|
||||
Some(Node::Window(_)) => Ok(node),
|
||||
Some(Node::Split { first, .. }) => self.first_window(*first),
|
||||
Some(Node::Stack {
|
||||
children, active, ..
|
||||
}) => children
|
||||
Some(Node::Stack { children, active, .. }) => children
|
||||
.get(*active)
|
||||
.copied()
|
||||
.ok_or(LayoutError::InvalidTree("stack active index is invalid"))
|
||||
@@ -662,12 +637,7 @@ impl LayoutTree {
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_child(
|
||||
&mut self,
|
||||
parent: NodeId,
|
||||
old: NodeId,
|
||||
new: NodeId,
|
||||
) -> Result<(), LayoutError> {
|
||||
fn replace_child(&mut self, parent: NodeId, old: NodeId, new: NodeId) -> Result<(), LayoutError> {
|
||||
match self.nodes.get_mut(&parent) {
|
||||
Some(Node::Split { first, .. }) if *first == old => *first = new,
|
||||
Some(Node::Split { second, .. }) if *second == old => *second = new,
|
||||
@@ -684,12 +654,7 @@ impl LayoutTree {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collapse_parent(
|
||||
&mut self,
|
||||
parent: NodeId,
|
||||
leaf: NodeId,
|
||||
sibling: NodeId,
|
||||
) -> Result<(), LayoutError> {
|
||||
fn collapse_parent(&mut self, parent: NodeId, leaf: NodeId, sibling: NodeId) -> Result<(), LayoutError> {
|
||||
let window = match self.nodes.get(&leaf) {
|
||||
Some(Node::Window(window)) => *window,
|
||||
_ => return Err(LayoutError::InvalidTree("removed leaf is not a window")),
|
||||
@@ -786,21 +751,11 @@ impl LayoutTree {
|
||||
} else {
|
||||
(available - second_required).max(0.0)
|
||||
};
|
||||
let adjusted_ratio = if available > 0.0 {
|
||||
first_extent / available
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
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,
|
||||
)?;
|
||||
self.layout_node_constrained(*second, second_bounds, constraints, focused, geometry)?;
|
||||
}
|
||||
Some(Node::Stack { children, active }) => {
|
||||
let child = children
|
||||
@@ -826,10 +781,7 @@ impl LayoutTree {
|
||||
Ok((constraint.min_width, constraint.min_height))
|
||||
}
|
||||
Some(Node::Split {
|
||||
axis,
|
||||
first,
|
||||
second,
|
||||
..
|
||||
axis, first, second, ..
|
||||
}) => {
|
||||
let first = self.minimum_size(*first, constraints)?;
|
||||
let second = self.minimum_size(*second, constraints)?;
|
||||
@@ -860,9 +812,7 @@ impl LayoutTree {
|
||||
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)),
|
||||
Some(Node::Stack { children, .. }) => children.iter().any(|child| self.contains_window(*child, window)),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
@@ -887,15 +837,9 @@ impl LayoutTree {
|
||||
}
|
||||
}
|
||||
Some(Node::Split {
|
||||
ratio,
|
||||
first,
|
||||
second,
|
||||
..
|
||||
ratio, first, second, ..
|
||||
}) => {
|
||||
if first == second
|
||||
|| !ratio.is_finite()
|
||||
|| !(MIN_SPLIT_RATIO..=MAX_SPLIT_RATIO).contains(ratio)
|
||||
{
|
||||
if first == second || !ratio.is_finite() || !(MIN_SPLIT_RATIO..=MAX_SPLIT_RATIO).contains(ratio) {
|
||||
return Err(LayoutError::InvalidTree("split is invalid"));
|
||||
}
|
||||
|
||||
@@ -931,11 +875,7 @@ fn valid_gap(gap: f64) -> f64 {
|
||||
}
|
||||
|
||||
fn valid_resize_amount(amount: f64) -> f64 {
|
||||
if amount.is_finite() {
|
||||
amount.abs()
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
if amount.is_finite() { amount.abs() } else { 0.0 }
|
||||
}
|
||||
|
||||
fn normalized_constraints(constraints: SizeConstraints) -> SizeConstraints {
|
||||
@@ -1029,12 +969,7 @@ fn nearly_equal(left: f64, right: f64) -> bool {
|
||||
(left - right).abs() < f64::EPSILON * left.abs().max(right.abs()).max(1.0)
|
||||
}
|
||||
|
||||
fn intervals_overlap(
|
||||
first_start: f64,
|
||||
first_size: f64,
|
||||
second_start: f64,
|
||||
second_size: f64,
|
||||
) -> bool {
|
||||
fn intervals_overlap(first_start: f64, first_size: f64, second_start: f64, second_size: f64) -> bool {
|
||||
first_start < second_start + second_size && second_start < first_start + first_size
|
||||
}
|
||||
|
||||
@@ -1043,24 +978,14 @@ fn split_rect(bounds: Rect, axis: Axis, ratio: f64) -> (Rect, Rect) {
|
||||
Axis::Horizontal => {
|
||||
let boundary = bounds.x + bounds.width * ratio;
|
||||
let first = Rect::new(bounds.x, bounds.y, boundary - bounds.x, bounds.height);
|
||||
let second = Rect::new(
|
||||
boundary,
|
||||
bounds.y,
|
||||
bounds.x + bounds.width - boundary,
|
||||
bounds.height,
|
||||
);
|
||||
let second = Rect::new(boundary, bounds.y, bounds.x + bounds.width - boundary, bounds.height);
|
||||
|
||||
(first, second)
|
||||
}
|
||||
Axis::Vertical => {
|
||||
let boundary = bounds.y + bounds.height * ratio;
|
||||
let first = Rect::new(bounds.x, bounds.y, bounds.width, boundary - bounds.y);
|
||||
let second = Rect::new(
|
||||
bounds.x,
|
||||
boundary,
|
||||
bounds.width,
|
||||
bounds.y + bounds.height - boundary,
|
||||
);
|
||||
let second = Rect::new(bounds.x, boundary, bounds.width, bounds.y + bounds.height - boundary);
|
||||
|
||||
(first, second)
|
||||
}
|
||||
@@ -1074,43 +999,32 @@ mod tests {
|
||||
#[test]
|
||||
fn insertion_splits_the_focused_leaf() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(3), Some(WindowId(1)), Axis::Vertical, 0.5)
|
||||
.unwrap();
|
||||
|
||||
assert!(tree.validate().is_ok());
|
||||
assert_eq!(
|
||||
tree.geometry(Rect::new(0.0, 0.0, 100.0, 80.0))
|
||||
.unwrap()
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
assert_eq!(tree.geometry(Rect::new(0.0, 0.0, 100.0, 80.0)).unwrap().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removal_collapses_the_parent_split() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.remove(WindowId(1)).unwrap();
|
||||
|
||||
assert!(tree.validate().is_ok());
|
||||
assert_eq!(
|
||||
tree.node(tree.root().unwrap()),
|
||||
Some(&Node::Window(WindowId(2)))
|
||||
);
|
||||
assert_eq!(tree.node(tree.root().unwrap()), Some(&Node::Window(WindowId(2))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_split_boundary_is_exact() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 1.0 / 3.0)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 1.0 / 3.0).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 1.0 / 3.0)
|
||||
.unwrap();
|
||||
|
||||
@@ -1125,8 +1039,7 @@ mod tests {
|
||||
#[test]
|
||||
fn rejects_duplicate_and_unknown_windows() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5),
|
||||
@@ -1147,28 +1060,20 @@ mod tests {
|
||||
#[test]
|
||||
fn automatic_axis_follows_focused_tile_shape() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
let wide = Rect::new(0.0, 0.0, 1_920.0, 1_080.0);
|
||||
assert_eq!(
|
||||
tree.automatic_axis(Some(WindowId(1)), wide).unwrap(),
|
||||
Axis::Horizontal
|
||||
);
|
||||
assert_eq!(tree.automatic_axis(Some(WindowId(1)), wide).unwrap(), Axis::Horizontal);
|
||||
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
tree.automatic_axis(Some(WindowId(1)), wide).unwrap(),
|
||||
Axis::Vertical
|
||||
);
|
||||
assert_eq!(tree.automatic_axis(Some(WindowId(1)), wide).unwrap(), Axis::Vertical);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gaps_preserve_outer_and_shared_spacing() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
|
||||
@@ -1186,8 +1091,7 @@ mod tests {
|
||||
#[test]
|
||||
fn smart_gaps_remove_outer_gap_for_one_window() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
let bounds = Rect::new(0.0, 0.0, 100.0, 80.0);
|
||||
let geometry = tree
|
||||
@@ -1206,8 +1110,7 @@ mod tests {
|
||||
#[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();
|
||||
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())
|
||||
@@ -1219,8 +1122,7 @@ mod tests {
|
||||
#[test]
|
||||
fn directional_neighbor_prefers_primary_distance() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(3), Some(WindowId(2)), Axis::Vertical, 0.5)
|
||||
@@ -1233,13 +1135,11 @@ mod tests {
|
||||
Some(WindowId(2))
|
||||
);
|
||||
assert_eq!(
|
||||
tree.directional_neighbor(WindowId(2), Direction::Down, bounds)
|
||||
.unwrap(),
|
||||
tree.directional_neighbor(WindowId(2), Direction::Down, bounds).unwrap(),
|
||||
Some(WindowId(3))
|
||||
);
|
||||
assert_eq!(
|
||||
tree.directional_neighbor(WindowId(1), Direction::Left, bounds)
|
||||
.unwrap(),
|
||||
tree.directional_neighbor(WindowId(1), Direction::Left, bounds).unwrap(),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -1247,16 +1147,12 @@ mod tests {
|
||||
#[test]
|
||||
fn directional_move_swaps_window_leaves() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
let bounds = Rect::new(0.0, 0.0, 100.0, 80.0);
|
||||
|
||||
assert!(
|
||||
tree.move_window(WindowId(1), Direction::Right, bounds)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(tree.move_window(WindowId(1), Direction::Right, bounds).unwrap());
|
||||
|
||||
let geometry = tree.geometry(bounds).unwrap();
|
||||
assert_eq!(geometry[&WindowId(2)].x, 0.0);
|
||||
@@ -1267,16 +1163,11 @@ mod tests {
|
||||
#[test]
|
||||
fn directional_move_at_boundary_is_a_noop() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
assert!(
|
||||
!tree
|
||||
.move_window(
|
||||
WindowId(1),
|
||||
Direction::Left,
|
||||
Rect::new(0.0, 0.0, 100.0, 80.0),
|
||||
)
|
||||
.move_window(WindowId(1), Direction::Left, Rect::new(0.0, 0.0, 100.0, 80.0),)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
@@ -1284,18 +1175,14 @@ mod tests {
|
||||
#[test]
|
||||
fn directional_resize_changes_the_nearest_matching_split() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(3), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
let bounds = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
|
||||
assert!(
|
||||
tree.resize_window(WindowId(1), Direction::Right, 0.1)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(tree.resize_window(WindowId(1), Direction::Right, 0.1).unwrap());
|
||||
|
||||
let geometry = tree.geometry(bounds).unwrap();
|
||||
assert_eq!(geometry[&WindowId(1)].width, 30.0);
|
||||
@@ -1307,23 +1194,15 @@ mod tests {
|
||||
#[test]
|
||||
fn directional_resize_walks_to_an_ancestor_and_clamps() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(3), Some(WindowId(1)), Axis::Vertical, 0.5)
|
||||
.unwrap();
|
||||
let bounds = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
|
||||
assert!(
|
||||
tree.resize_window(WindowId(3), Direction::Right, 1.0)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
!tree
|
||||
.resize_window(WindowId(3), Direction::Right, 1.0)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(tree.resize_window(WindowId(3), Direction::Right, 1.0).unwrap());
|
||||
assert!(!tree.resize_window(WindowId(3), Direction::Right, 1.0).unwrap());
|
||||
|
||||
let geometry = tree.geometry(bounds).unwrap();
|
||||
assert_eq!(geometry[&WindowId(2)].x, 95.0);
|
||||
@@ -1333,26 +1212,16 @@ mod tests {
|
||||
#[test]
|
||||
fn directional_resize_at_an_outer_edge_is_a_noop() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
assert!(
|
||||
!tree
|
||||
.resize_window(WindowId(1), Direction::Left, 0.05)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
!tree
|
||||
.resize_window(WindowId(1), Direction::Right, f64::NAN)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(!tree.resize_window(WindowId(1), Direction::Left, 0.05).unwrap());
|
||||
assert!(!tree.resize_window(WindowId(1), Direction::Right, f64::NAN).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacking_windows_shows_only_the_active_child() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.stack_window(WindowId(2), WindowId(1)).unwrap();
|
||||
@@ -1366,8 +1235,7 @@ mod tests {
|
||||
#[test]
|
||||
fn activating_a_hidden_stack_child_changes_visible_geometry() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.stack_window(WindowId(2), WindowId(1)).unwrap();
|
||||
@@ -1382,8 +1250,7 @@ mod tests {
|
||||
#[test]
|
||||
fn removing_from_a_stack_collapses_or_preserves_cardinality() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.stack_window(WindowId(2), WindowId(1)).unwrap();
|
||||
@@ -1393,10 +1260,7 @@ mod tests {
|
||||
tree.remove(WindowId(3)).unwrap();
|
||||
tree.remove(WindowId(2)).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
tree.node(tree.root().unwrap()),
|
||||
Some(&Node::Window(WindowId(1)))
|
||||
);
|
||||
assert_eq!(tree.node(tree.root().unwrap()), Some(&Node::Window(WindowId(1))));
|
||||
assert!(tree.validate().is_ok());
|
||||
}
|
||||
|
||||
@@ -1409,9 +1273,7 @@ mod tests {
|
||||
let bounds = Rect::new(0.0, 0.0, 1_920.0, 1_080.0);
|
||||
|
||||
for _ in 0..2_000 {
|
||||
random = random
|
||||
.wrapping_mul(6_364_136_223_846_793_005)
|
||||
.wrapping_add(1);
|
||||
random = random.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
|
||||
let operation = random % 6;
|
||||
|
||||
match operation {
|
||||
@@ -1425,8 +1287,7 @@ mod tests {
|
||||
Axis::Vertical
|
||||
};
|
||||
|
||||
tree.insert(window, focused, axis, ratio_from_random(random))
|
||||
.unwrap();
|
||||
tree.insert(window, focused, axis, ratio_from_random(random)).unwrap();
|
||||
windows.push(window);
|
||||
}
|
||||
1 if !windows.is_empty() => {
|
||||
@@ -1453,8 +1314,7 @@ mod tests {
|
||||
}
|
||||
5 if !windows.is_empty() => {
|
||||
let window = windows[random as usize % windows.len()];
|
||||
tree.resize_window(window, random_direction(random), 0.03)
|
||||
.unwrap();
|
||||
tree.resize_window(window, random_direction(random), 0.03).unwrap();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -1466,8 +1326,7 @@ 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(1), None, Axis::Horizontal, 0.2).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.2)
|
||||
.unwrap();
|
||||
let constraints = HashMap::from([
|
||||
@@ -1507,8 +1366,7 @@ mod tests {
|
||||
#[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(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
let constraints = HashMap::from([
|
||||
@@ -1550,8 +1408,7 @@ mod tests {
|
||||
#[test]
|
||||
fn maximum_constraints_clip_without_overlapping_neighbors() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
let constraints = HashMap::from([(
|
||||
WindowId(1),
|
||||
SizeConstraints {
|
||||
@@ -1569,10 +1426,7 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result.geometry[&WindowId(1)],
|
||||
Rect::new(4.0, 4.0, 60.0, 40.0)
|
||||
);
|
||||
assert_eq!(result.geometry[&WindowId(1)], Rect::new(4.0, 4.0, 60.0, 40.0));
|
||||
assert_eq!(result.warnings.len(), 2);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::{
|
||||
ConstraintKind, ConstraintWarning, Direction, GapConfig, LayoutError, LayoutResult, Rect,
|
||||
SizeConstraints, WindowId, normalized_constraints, record_minimum_warnings,
|
||||
ConstraintKind, ConstraintWarning, Direction, GapConfig, LayoutError, LayoutResult, Rect, SizeConstraints,
|
||||
WindowId, normalized_constraints, record_minimum_warnings,
|
||||
};
|
||||
|
||||
const DEFAULT_WIDTH: f64 = 0.5;
|
||||
@@ -145,16 +145,10 @@ impl ScrollingLayout {
|
||||
}
|
||||
|
||||
pub fn window_ids(&self) -> impl Iterator<Item = WindowId> + '_ {
|
||||
self.columns
|
||||
.iter()
|
||||
.flat_map(|column| column.windows.iter().copied())
|
||||
self.columns.iter().flat_map(|column| column.windows.iter().copied())
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
focused: Option<WindowId>,
|
||||
) -> Result<(), LayoutError> {
|
||||
pub fn insert(&mut self, window: WindowId, focused: Option<WindowId>) -> Result<(), LayoutError> {
|
||||
if self.contains(window) {
|
||||
return Err(LayoutError::DuplicateWindow(window));
|
||||
}
|
||||
@@ -163,8 +157,7 @@ impl ScrollingLayout {
|
||||
.and_then(|focused| self.window_location(focused))
|
||||
.map(|(column, _)| column + 1)
|
||||
.unwrap_or(self.columns.len());
|
||||
self.columns
|
||||
.insert(index, Column::new(window, self.default_width));
|
||||
self.columns.insert(index, Column::new(window, self.default_width));
|
||||
self.active_column = Some(index);
|
||||
self.reveal_pending = (self.columns.len() > 1).then_some(window);
|
||||
|
||||
@@ -173,12 +166,9 @@ impl ScrollingLayout {
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||
let (column_index, window_index) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let (column_index, window_index) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let column = &mut self.columns[column_index];
|
||||
let removed_active_column =
|
||||
self.active_column == Some(column_index) && column.windows.len() == 1;
|
||||
let removed_active_column = self.active_column == Some(column_index) && column.windows.len() == 1;
|
||||
column.windows.remove(window_index);
|
||||
column.heights.remove(window_index);
|
||||
|
||||
@@ -206,9 +196,7 @@ impl ScrollingLayout {
|
||||
}
|
||||
|
||||
pub fn focus(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||
let (column, index) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let (column, index) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let changed_column = self.active_column != Some(column);
|
||||
self.active_column = Some(column);
|
||||
self.columns[column].active = index;
|
||||
@@ -221,9 +209,7 @@ impl ScrollingLayout {
|
||||
/// Hover changes the active window without requesting viewport movement.
|
||||
/// Keep any earlier explicit reveal attached to its original window.
|
||||
pub fn focus_without_reveal(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||
let (column, index) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let (column, index) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
self.active_column = Some(column);
|
||||
self.columns[column].active = index;
|
||||
Ok(())
|
||||
@@ -237,11 +223,7 @@ impl ScrollingLayout {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn slide_focus_from(
|
||||
&mut self,
|
||||
previous: WindowId,
|
||||
focused: WindowId,
|
||||
) -> Result<(), LayoutError> {
|
||||
pub fn slide_focus_from(&mut self, previous: WindowId, focused: WindowId) -> Result<(), LayoutError> {
|
||||
self.window_location(previous)
|
||||
.ok_or(LayoutError::UnknownWindow(previous))?;
|
||||
self.focus_without_reveal(focused)?;
|
||||
@@ -250,16 +232,11 @@ impl ScrollingLayout {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn move_into_column(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
target: WindowId,
|
||||
) -> Result<(), LayoutError> {
|
||||
pub fn move_into_column(&mut self, window: WindowId, target: WindowId) -> Result<(), LayoutError> {
|
||||
if window == target {
|
||||
return Ok(());
|
||||
}
|
||||
self.window_location(target)
|
||||
.ok_or(LayoutError::UnknownWindow(target))?;
|
||||
self.window_location(target).ok_or(LayoutError::UnknownWindow(target))?;
|
||||
self.remove(window)?;
|
||||
let (target_column, target_index) = self
|
||||
.window_location(target)
|
||||
@@ -278,9 +255,7 @@ impl ScrollingLayout {
|
||||
}
|
||||
|
||||
pub fn extract_to_column(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||
let (source, _) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let (source, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
if self.columns[source].windows.len() == 1 {
|
||||
self.active_column = Some(source);
|
||||
return Ok(());
|
||||
@@ -288,8 +263,7 @@ impl ScrollingLayout {
|
||||
|
||||
self.remove(window)?;
|
||||
let insertion = (source + 1).min(self.columns.len());
|
||||
self.columns
|
||||
.insert(insertion, Column::new(window, self.default_width));
|
||||
self.columns.insert(insertion, Column::new(window, self.default_width));
|
||||
self.active_column = Some(insertion);
|
||||
self.reveal_pending = self.active_window();
|
||||
|
||||
@@ -297,32 +271,16 @@ impl ScrollingLayout {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_column_width(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
width: ColumnWidth,
|
||||
) -> Result<(), LayoutError> {
|
||||
let (column, _) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
pub fn set_column_width(&mut self, window: WindowId, width: ColumnWidth) -> Result<(), LayoutError> {
|
||||
let (column, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
self.columns[column].width = normalized_width(width);
|
||||
self.reveal_pending = self.active_window();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cycle_column_width(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
presets: &[ColumnWidth],
|
||||
) -> Result<bool, LayoutError> {
|
||||
let (column, _) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let presets = presets
|
||||
.iter()
|
||||
.copied()
|
||||
.map(normalized_width)
|
||||
.collect::<Vec<_>>();
|
||||
pub fn cycle_column_width(&mut self, window: WindowId, presets: &[ColumnWidth]) -> Result<bool, LayoutError> {
|
||||
let (column, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let presets = presets.iter().copied().map(normalized_width).collect::<Vec<_>>();
|
||||
if presets.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -365,35 +323,22 @@ impl ScrollingLayout {
|
||||
window: WindowId,
|
||||
direction: Direction,
|
||||
) -> Result<Option<WindowId>, LayoutError> {
|
||||
let (column, row) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
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::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]),
|
||||
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))?;
|
||||
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 => {
|
||||
@@ -407,9 +352,7 @@ impl ScrollingLayout {
|
||||
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)?
|
||||
}
|
||||
Direction::Right if column + 1 < self.columns.len() => self.move_horizontally(window, column + 1)?,
|
||||
_ => return Ok(false),
|
||||
}
|
||||
|
||||
@@ -417,20 +360,9 @@ impl ScrollingLayout {
|
||||
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
|
||||
};
|
||||
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);
|
||||
}
|
||||
@@ -441,18 +373,12 @@ impl ScrollingLayout {
|
||||
ColumnWidth::Proportion(value) => value,
|
||||
ColumnWidth::Fixed(_) | ColumnWidth::Full => DEFAULT_WIDTH,
|
||||
};
|
||||
let adjustment = if direction == Direction::Right {
|
||||
amount
|
||||
} else {
|
||||
-amount
|
||||
};
|
||||
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::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)
|
||||
}
|
||||
@@ -496,9 +422,7 @@ impl ScrollingLayout {
|
||||
|
||||
for column in &self.columns {
|
||||
let requested = match normalized_width(column.width) {
|
||||
ColumnWidth::Proportion(proportion) => {
|
||||
((viewport_width + inner) * proportion - inner).max(1.0)
|
||||
}
|
||||
ColumnWidth::Proportion(proportion) => ((viewport_width + inner) * proportion - inner).max(1.0),
|
||||
ColumnWidth::Fixed(width) => width,
|
||||
ColumnWidth::Full => viewport_width,
|
||||
};
|
||||
@@ -526,18 +450,14 @@ impl ScrollingLayout {
|
||||
self.reveal_pending = None;
|
||||
self.swipe_focus_pending = None;
|
||||
} else if let Some((previous, focused)) = self.swipe_focus_pending.take() {
|
||||
if let (Some((from, _)), Some((to, _))) = (
|
||||
self.window_location(previous),
|
||||
self.window_location(focused),
|
||||
) {
|
||||
if let (Some((from, _)), Some((to, _))) = (self.window_location(previous), self.window_location(focused)) {
|
||||
let last_end = column_positions
|
||||
.last()
|
||||
.map(|(start, width)| start + width)
|
||||
.unwrap_or(0.0);
|
||||
let max_offset = (last_end - viewport_width).max(0.0);
|
||||
self.viewport_x = (self.viewport_x + column_positions[to].0
|
||||
- column_positions[from].0)
|
||||
.clamp(0.0, max_offset);
|
||||
self.viewport_x =
|
||||
(self.viewport_x + column_positions[to].0 - column_positions[from].0).clamp(0.0, max_offset);
|
||||
}
|
||||
self.reveal_pending = None;
|
||||
} else {
|
||||
@@ -554,9 +474,7 @@ impl ScrollingLayout {
|
||||
let mut result = LayoutResult::default();
|
||||
for (column_index, column) in self.columns.iter().enumerate() {
|
||||
let (column_x, column_width) = column_positions[column_index];
|
||||
let available_height = (viewport_height
|
||||
- inner * (column.windows.len().saturating_sub(1) as f64))
|
||||
.max(1.0);
|
||||
let available_height = (viewport_height - inner * (column.windows.len().saturating_sub(1) as f64)).max(1.0);
|
||||
let mut y = bounds.y + outer;
|
||||
|
||||
for (window_index, window) in column.windows.iter().enumerate() {
|
||||
@@ -615,12 +533,7 @@ impl ScrollingLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fn paged_viewport(
|
||||
&self,
|
||||
positions: &[(f64, f64)],
|
||||
viewport_width: f64,
|
||||
active: Option<usize>,
|
||||
) -> Option<f64> {
|
||||
fn paged_viewport(&self, positions: &[(f64, f64)], viewport_width: f64, active: Option<usize>) -> Option<f64> {
|
||||
if self.focus_strategy != ViewportFocusStrategy::Paged {
|
||||
return None;
|
||||
}
|
||||
@@ -671,14 +584,8 @@ impl ScrollingLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fn move_horizontally(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
destination: usize,
|
||||
) -> Result<(), LayoutError> {
|
||||
let (source, _) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
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);
|
||||
@@ -686,15 +593,10 @@ impl ScrollingLayout {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let insertion = if destination < source {
|
||||
source
|
||||
} else {
|
||||
source + 1
|
||||
};
|
||||
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.columns.insert(insertion, Column::new(window, self.default_width));
|
||||
self.active_column = Some(insertion);
|
||||
self.reveal_pending = self.active_window();
|
||||
Ok(())
|
||||
@@ -711,31 +613,20 @@ impl ScrollingLayout {
|
||||
|
||||
fn validate_scalars(&self) -> Result<(), LayoutError> {
|
||||
if !self.viewport_x.is_finite() {
|
||||
return Err(LayoutError::InvalidTree(
|
||||
"scrolling viewport offset is invalid",
|
||||
));
|
||||
return Err(LayoutError::InvalidTree("scrolling viewport offset is invalid"));
|
||||
}
|
||||
if normalized_width(self.default_width) != self.default_width {
|
||||
return Err(LayoutError::InvalidTree(
|
||||
"scrolling default column width is invalid",
|
||||
));
|
||||
return Err(LayoutError::InvalidTree("scrolling default column width is invalid"));
|
||||
}
|
||||
if self.columns.is_empty() {
|
||||
return if self.active_column.is_none() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(LayoutError::InvalidTree(
|
||||
"empty scrolling layout has an active column",
|
||||
))
|
||||
Err(LayoutError::InvalidTree("empty scrolling layout has an active column"))
|
||||
};
|
||||
}
|
||||
if self
|
||||
.active_column
|
||||
.is_none_or(|active| active >= self.columns.len())
|
||||
{
|
||||
return Err(LayoutError::InvalidTree(
|
||||
"scrolling active column is invalid",
|
||||
));
|
||||
if self.active_column.is_none_or(|active| active >= self.columns.len()) {
|
||||
return Err(LayoutError::InvalidTree("scrolling active column is invalid"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -746,24 +637,15 @@ impl ScrollingLayout {
|
||||
let mut windows = HashSet::new();
|
||||
for column in &self.columns {
|
||||
if normalized_width(column.width) != column.width {
|
||||
return Err(LayoutError::InvalidTree(
|
||||
"scrolling column width is invalid",
|
||||
));
|
||||
return Err(LayoutError::InvalidTree("scrolling column width is invalid"));
|
||||
}
|
||||
if column.windows.is_empty() {
|
||||
return Err(LayoutError::InvalidTree("scrolling column is empty"));
|
||||
}
|
||||
if column.active >= column.windows.len() || column.heights.len() != column.windows.len()
|
||||
{
|
||||
return Err(LayoutError::InvalidTree(
|
||||
"scrolling column metadata is invalid",
|
||||
));
|
||||
if column.active >= column.windows.len() || column.heights.len() != column.windows.len() {
|
||||
return Err(LayoutError::InvalidTree("scrolling column metadata is invalid"));
|
||||
}
|
||||
if !column
|
||||
.heights
|
||||
.iter()
|
||||
.all(|height| height.is_finite() && *height > 0.0)
|
||||
{
|
||||
if !column.heights.iter().all(|height| height.is_finite() && *height > 0.0) {
|
||||
return Err(LayoutError::InvalidTree("scrolling height is invalid"));
|
||||
}
|
||||
for window in &column.windows {
|
||||
@@ -790,9 +672,7 @@ fn resize_rows(column: &mut Column, row: usize, neighbor: usize, amount: f64) ->
|
||||
|
||||
fn normalized_width(width: ColumnWidth) -> ColumnWidth {
|
||||
match width {
|
||||
ColumnWidth::Proportion(value) if value.is_finite() && value > 0.0 => {
|
||||
ColumnWidth::Proportion(value)
|
||||
}
|
||||
ColumnWidth::Proportion(value) if value.is_finite() && value > 0.0 => ColumnWidth::Proportion(value),
|
||||
ColumnWidth::Fixed(value) if value.is_finite() && value > 0.0 => ColumnWidth::Fixed(value),
|
||||
ColumnWidth::Full => ColumnWidth::Full,
|
||||
_ => ColumnWidth::default(),
|
||||
@@ -800,11 +680,7 @@ fn normalized_width(width: ColumnWidth) -> ColumnWidth {
|
||||
}
|
||||
|
||||
fn finite_nonnegative(value: f64) -> f64 {
|
||||
if value.is_finite() {
|
||||
value.max(0.0)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
if value.is_finite() { value.max(0.0) } else { 0.0 }
|
||||
}
|
||||
|
||||
fn apply_maximums(
|
||||
@@ -866,12 +742,7 @@ mod tests {
|
||||
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)
|
||||
);
|
||||
assert!(layout.columns().iter().all(|column| column.width == ColumnWidth::Full));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -920,9 +791,7 @@ mod tests {
|
||||
fn focus_reveal_scrolls_only_as_far_as_needed() {
|
||||
let mut layout = ScrollingLayout::default();
|
||||
for id in 1..=4 {
|
||||
layout
|
||||
.insert(window(id), Some(window(id.saturating_sub(1))))
|
||||
.unwrap();
|
||||
layout.insert(window(id), Some(window(id.saturating_sub(1)))).unwrap();
|
||||
}
|
||||
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
|
||||
let gaps = GapConfig {
|
||||
@@ -961,14 +830,8 @@ mod tests {
|
||||
|
||||
assert_eq!(layout.viewport_x(), 0.0);
|
||||
assert_eq!(first.geometry, second.geometry);
|
||||
assert_eq!(
|
||||
second.geometry[&window(1)],
|
||||
Rect::new(10.0, 10.0, 485.0, 780.0)
|
||||
);
|
||||
assert_eq!(
|
||||
second.geometry[&window(2)],
|
||||
Rect::new(505.0, 10.0, 485.0, 780.0)
|
||||
);
|
||||
assert_eq!(second.geometry[&window(1)], Rect::new(10.0, 10.0, 485.0, 780.0));
|
||||
assert_eq!(second.geometry[&window(2)], Rect::new(505.0, 10.0, 485.0, 780.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1037,13 +900,9 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(layout.viewport_x(), 0.0);
|
||||
|
||||
for (previous, focused, expected) in
|
||||
[(1, 2, 495.0), (2, 3, 990.0), (3, 2, 495.0), (2, 1, 0.0)]
|
||||
{
|
||||
for (previous, focused, expected) in [(1, 2, 495.0), (2, 3, 990.0), (3, 2, 495.0), (2, 1, 0.0)] {
|
||||
layout.focus_and_reveal(window(focused)).unwrap();
|
||||
layout
|
||||
.slide_focus_from(window(previous), window(focused))
|
||||
.unwrap();
|
||||
layout.slide_focus_from(window(previous), window(focused)).unwrap();
|
||||
let result = layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused)))
|
||||
.unwrap();
|
||||
@@ -1071,9 +930,7 @@ mod tests {
|
||||
#[test]
|
||||
fn paged_focus_preserves_explicit_center_until_focus_changes() {
|
||||
let (mut layout, bounds, gaps) = paged_layout(4);
|
||||
layout
|
||||
.center_window(window(2), bounds, gaps, &HashMap::new())
|
||||
.unwrap();
|
||||
layout.center_window(window(2), bounds, gaps, &HashMap::new()).unwrap();
|
||||
let result = layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
|
||||
.unwrap();
|
||||
@@ -1188,18 +1045,14 @@ mod tests {
|
||||
};
|
||||
|
||||
for id in 1..=3 {
|
||||
layout
|
||||
.insert(window(id), (id > 1).then(|| window(id - 1)))
|
||||
.unwrap();
|
||||
layout.insert(window(id), (id > 1).then(|| window(id - 1))).unwrap();
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(id)))
|
||||
.unwrap();
|
||||
}
|
||||
let before = layout.viewport_x();
|
||||
|
||||
layout
|
||||
.resize_window(window(3), Direction::Right, 0.5)
|
||||
.unwrap();
|
||||
layout.resize_window(window(3), Direction::Right, 0.5).unwrap();
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(3)))
|
||||
.unwrap();
|
||||
@@ -1345,18 +1198,10 @@ mod tests {
|
||||
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!(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!(layout.resize_window(window(1), Direction::Down, 0.1).unwrap());
|
||||
assert_eq!(layout.columns()[0].heights, vec![0.6, 0.4]);
|
||||
}
|
||||
|
||||
@@ -1371,10 +1216,7 @@ mod tests {
|
||||
];
|
||||
|
||||
assert!(layout.cycle_column_width(window(1), &presets).unwrap());
|
||||
assert_eq!(
|
||||
layout.columns()[0].width,
|
||||
ColumnWidth::Proportion(2.0 / 3.0)
|
||||
);
|
||||
assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(2.0 / 3.0));
|
||||
assert!(layout.cycle_column_width(window(1), &presets).unwrap());
|
||||
assert_eq!(layout.columns()[0].width, ColumnWidth::Full);
|
||||
assert!(layout.cycle_column_width(window(1), &presets).unwrap());
|
||||
@@ -1459,10 +1301,7 @@ mod tests {
|
||||
let mut layout = ScrollingLayout::default();
|
||||
for id in 1_u64..=4 {
|
||||
layout
|
||||
.insert(
|
||||
window(id),
|
||||
id.checked_sub(1).filter(|id| *id > 0).map(window),
|
||||
)
|
||||
.insert(window(id), id.checked_sub(1).filter(|id| *id > 0).map(window))
|
||||
.unwrap();
|
||||
}
|
||||
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
|
||||
@@ -1475,11 +1314,7 @@ mod tests {
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4)))
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
layout
|
||||
.center_window(window(2), bounds, gaps, &HashMap::new())
|
||||
.unwrap()
|
||||
);
|
||||
assert!(layout.center_window(window(2), bounds, gaps, &HashMap::new()).unwrap());
|
||||
let result = layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
|
||||
.unwrap();
|
||||
@@ -1518,9 +1353,7 @@ mod tests {
|
||||
if first == second {
|
||||
second = (second + 1) % windows.len();
|
||||
}
|
||||
layout
|
||||
.move_into_column(windows[first], windows[second])
|
||||
.unwrap();
|
||||
layout.move_into_column(windows[first], windows[second]).unwrap();
|
||||
}
|
||||
3 if !windows.is_empty() => {
|
||||
let index = random as usize % windows.len();
|
||||
@@ -1534,9 +1367,7 @@ mod tests {
|
||||
5 if !windows.is_empty() => {
|
||||
let index = random as usize % windows.len();
|
||||
let direction = random_direction(random.rotate_left(23));
|
||||
layout
|
||||
.resize_window(windows[index], direction, 0.05)
|
||||
.unwrap();
|
||||
layout.resize_window(windows[index], direction, 0.05).unwrap();
|
||||
}
|
||||
6 if !windows.is_empty() => {
|
||||
let index = random as usize % windows.len();
|
||||
@@ -1546,10 +1377,7 @@ mod tests {
|
||||
}
|
||||
|
||||
layout.validate().unwrap();
|
||||
assert_eq!(
|
||||
layout.window_ids().collect::<HashSet<_>>().len(),
|
||||
windows.len()
|
||||
);
|
||||
assert_eq!(layout.window_ids().collect::<HashSet<_>>().len(), windows.len());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -195,15 +195,11 @@ pub mod window_capture {
|
||||
use wayland_client::protocol::__interfaces::*;
|
||||
use wayland_protocols_wlr::screencopy::v1::client::__interfaces::*;
|
||||
|
||||
wayland_scanner::generate_interfaces!(
|
||||
"../../protocols/ferese-window-capture-v1.xml"
|
||||
);
|
||||
wayland_scanner::generate_interfaces!("../../protocols/ferese-window-capture-v1.xml");
|
||||
}
|
||||
use self::__interfaces::*;
|
||||
|
||||
wayland_scanner::generate_client_code!(
|
||||
"../../protocols/ferese-window-capture-v1.xml"
|
||||
);
|
||||
wayland_scanner::generate_client_code!("../../protocols/ferese-window-capture-v1.xml");
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
@@ -216,15 +212,11 @@ pub mod window_capture {
|
||||
use wayland_protocols_wlr::screencopy::v1::server::__interfaces::*;
|
||||
use wayland_server::protocol::__interfaces::*;
|
||||
|
||||
wayland_scanner::generate_interfaces!(
|
||||
"../../protocols/ferese-window-capture-v1.xml"
|
||||
);
|
||||
wayland_scanner::generate_interfaces!("../../protocols/ferese-window-capture-v1.xml");
|
||||
}
|
||||
use self::__interfaces::*;
|
||||
|
||||
wayland_scanner::generate_server_code!(
|
||||
"../../protocols/ferese-window-capture-v1.xml"
|
||||
);
|
||||
wayland_scanner::generate_server_code!("../../protocols/ferese-window-capture-v1.xml");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use cosmic::iced::{Background, Color};
|
||||
use cosmic::theme;
|
||||
use cosmic::widget::button;
|
||||
|
||||
use crate::{composite, foreground};
|
||||
use cosmic::{
|
||||
iced::{Background, Color},
|
||||
theme,
|
||||
widget::button,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum State {
|
||||
@@ -13,11 +12,7 @@ pub(crate) enum State {
|
||||
Disabled,
|
||||
}
|
||||
|
||||
pub(crate) fn accent_button_style(
|
||||
theme: &cosmic::Theme,
|
||||
state: State,
|
||||
focused: bool,
|
||||
) -> button::Style {
|
||||
pub(crate) fn accent_button_style(theme: &cosmic::Theme, state: State, focused: bool) -> button::Style {
|
||||
let native = theme.cosmic();
|
||||
let component = &native.accent_button;
|
||||
let fill: Color = match state {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
//! Date/time labels and a calendar grid; callers own navigation and popup behavior.
|
||||
use cosmic::{
|
||||
Element,
|
||||
iced::{Alignment, Background, Border, Length},
|
||||
theme,
|
||||
widget::{column, container, row},
|
||||
};
|
||||
use cosmic::iced::{Alignment, Background, Border, Length};
|
||||
use cosmic::widget::{column, container, row};
|
||||
use cosmic::{Element, theme};
|
||||
use jiff::Zoned;
|
||||
|
||||
pub fn format_bar_time(now: &Zoned) -> String {
|
||||
@@ -41,10 +38,7 @@ pub fn stacked_digits(label: &str) -> Option<[String; 2]> {
|
||||
let (hour, minute) = time.split_once(':')?;
|
||||
if !(1..=2).contains(&hour.len())
|
||||
|| minute.len() != 2
|
||||
|| !hour
|
||||
.bytes()
|
||||
.chain(minute.bytes())
|
||||
.all(|digit| digit.is_ascii_digit())
|
||||
|| !hour.bytes().chain(minute.bytes()).all(|digit| digit.is_ascii_digit())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -104,13 +98,9 @@ pub fn grid<'a, M: Clone + 'a>(
|
||||
let mut weekdays = row::with_capacity(7).spacing(3);
|
||||
for label in ["M", "T", "W", "T", "F", "S", "S"] {
|
||||
weekdays = weekdays.push(
|
||||
container(
|
||||
crate::text(label, font)
|
||||
.size(11)
|
||||
.class(theme::Text::Color(muted)),
|
||||
)
|
||||
.width(30)
|
||||
.center_x(30),
|
||||
container(crate::text(label, font).size(11).class(theme::Text::Color(muted)))
|
||||
.width(30)
|
||||
.center_x(30),
|
||||
);
|
||||
}
|
||||
grid = grid.push(weekdays);
|
||||
@@ -118,26 +108,21 @@ pub fn grid<'a, M: Clone + 'a>(
|
||||
let mut dates = row::with_capacity(7).spacing(3);
|
||||
for weekday in 0..7 {
|
||||
let cell = week * 7 + weekday;
|
||||
let day = cell
|
||||
.checked_sub(start)
|
||||
.map(|day| day + 1)
|
||||
.filter(|day| *day <= days);
|
||||
let is_today =
|
||||
year == today.year() && month == today.month() && day == Some(today.day() as usize);
|
||||
let day = cell.checked_sub(start).map(|day| day + 1).filter(|day| *day <= days);
|
||||
let is_today = year == today.year() && month == today.month() && day == Some(today.day() as usize);
|
||||
let label = day.map_or(String::new(), |day| day.to_string());
|
||||
dates = dates.push(
|
||||
container(
|
||||
crate::text(label, font)
|
||||
.size(12)
|
||||
.class(theme::Text::Color(if is_today { primary } else { muted })),
|
||||
)
|
||||
container(crate::text(label, font).size(12).class(theme::Text::Color(if is_today {
|
||||
primary
|
||||
} else {
|
||||
muted
|
||||
})))
|
||||
.width(30)
|
||||
.height(25)
|
||||
.center_x(30)
|
||||
.center_y(25)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: is_today
|
||||
.then_some(Background::Color(palette.accent.scale_alpha(0.2 * opacity))),
|
||||
background: is_today.then_some(Background::Color(palette.accent.scale_alpha(0.2 * opacity))),
|
||||
border: Border {
|
||||
radius: palette.radius.min(12.).into(),
|
||||
..Default::default()
|
||||
@@ -155,14 +140,8 @@ pub fn grid<'a, M: Clone + 'a>(
|
||||
mod tests {
|
||||
#[test]
|
||||
fn pixel_clock_stacks_only_hour_and_minute_formats() {
|
||||
assert_eq!(
|
||||
super::stacked_digits("2:45 pm"),
|
||||
Some(["02".into(), "45".into()])
|
||||
);
|
||||
assert_eq!(
|
||||
super::stacked_digits("14:45"),
|
||||
Some(["14".into(), "45".into()])
|
||||
);
|
||||
assert_eq!(super::stacked_digits("2:45 pm"), Some(["02".into(), "45".into()]));
|
||||
assert_eq!(super::stacked_digits("14:45"), Some(["14".into(), "45".into()]));
|
||||
assert!(super::stacked_digits("14:45:30").is_none());
|
||||
assert!(super::stacked_digits("Today 14:45").is_none());
|
||||
assert!(super::stacked_digits("14:45 pm extra").is_none());
|
||||
@@ -171,18 +150,9 @@ mod tests {
|
||||
#[test]
|
||||
fn clock_uses_lowercase_date_and_twelve_hour_time() {
|
||||
for (stamp, expected) in [
|
||||
(
|
||||
"2026-09-28T21:32:00+01:00[Africa/Lagos]",
|
||||
"28 sept, 9:32 pm",
|
||||
),
|
||||
(
|
||||
"2026-09-28T00:05:00+01:00[Africa/Lagos]",
|
||||
"28 sept, 12:05 am",
|
||||
),
|
||||
(
|
||||
"2026-09-28T12:00:00+01:00[Africa/Lagos]",
|
||||
"28 sept, 12:00 pm",
|
||||
),
|
||||
("2026-09-28T21:32:00+01:00[Africa/Lagos]", "28 sept, 9:32 pm"),
|
||||
("2026-09-28T00:05:00+01:00[Africa/Lagos]", "28 sept, 12:05 am"),
|
||||
("2026-09-28T12:00:00+01:00[Africa/Lagos]", "28 sept, 12:00 pm"),
|
||||
] {
|
||||
assert_eq!(super::format_bar_time(&stamp.parse().unwrap()), expected);
|
||||
}
|
||||
@@ -194,10 +164,7 @@ mod tests {
|
||||
let (first, start, days) = super::month(february, 0);
|
||||
assert_eq!((first.year(), first.month(), start, days), (2024, 2, 3, 29));
|
||||
let (first, start, days) = super::month(february, -2);
|
||||
assert_eq!(
|
||||
(first.year(), first.month(), start, days),
|
||||
(2023, 12, 4, 31)
|
||||
);
|
||||
assert_eq!((first.year(), first.month(), start, days), (2023, 12, 4, 31));
|
||||
let june = jiff::civil::Date::new(2025, 6, 1).unwrap();
|
||||
let (_, start, days) = super::month(june, 0);
|
||||
assert_eq!((start, days), (6, 30));
|
||||
|
||||
@@ -76,27 +76,12 @@ pub fn apply(native: &mut cosmic::cosmic_theme::Theme, preferred: Color) {
|
||||
let surface: Color = native.primary(false).base.into();
|
||||
for component in [&mut native.accent, &mut native.accent_button] {
|
||||
let (base, on) = accent_pair(composite(component.base.into(), surface), preferred);
|
||||
component.hover = rgba(readable_fill(
|
||||
composite(component.hover.into(), surface),
|
||||
base,
|
||||
on,
|
||||
));
|
||||
component.pressed = rgba(readable_fill(
|
||||
composite(component.pressed.into(), surface),
|
||||
base,
|
||||
on,
|
||||
));
|
||||
component.selected = rgba(readable_fill(
|
||||
composite(component.selected.into(), surface),
|
||||
base,
|
||||
on,
|
||||
));
|
||||
component.hover = rgba(readable_fill(composite(component.hover.into(), surface), base, on));
|
||||
component.pressed = rgba(readable_fill(composite(component.pressed.into(), surface), base, on));
|
||||
component.selected = rgba(readable_fill(composite(component.selected.into(), surface), base, on));
|
||||
component.base = rgba(base);
|
||||
component.on = rgba(on);
|
||||
component.selected_text = rgba(foreground(
|
||||
composite(component.selected.into(), surface),
|
||||
on,
|
||||
));
|
||||
component.selected_text = rgba(foreground(composite(component.selected.into(), surface), on));
|
||||
component.disabled = rgba(composite(Color { a: 0.5, ..base }, surface));
|
||||
component.on_disabled = rgba(foreground(composite(Color { a: 0.5, ..base }, surface), on));
|
||||
}
|
||||
@@ -125,9 +110,10 @@ fn rgba(c: Color) -> cosmic::cosmic_theme::palette::Srgba {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use cosmic::iced::Background;
|
||||
|
||||
use super::*;
|
||||
use crate::button::{State, accent_button_style};
|
||||
use cosmic::iced::Background;
|
||||
#[test]
|
||||
fn reference_contrast_values_use_linear_srgb() {
|
||||
assert!((contrast(Color::BLACK, Color::WHITE) - 21.).abs() < 0.00001);
|
||||
@@ -139,10 +125,7 @@ mod tests {
|
||||
let preferred = Color::from_rgb8(240, 242, 246);
|
||||
assert_eq!(foreground(Color::BLACK, preferred), preferred);
|
||||
assert_eq!(foreground(Color::WHITE, preferred), Color::BLACK);
|
||||
assert_eq!(
|
||||
foreground(Color::from_rgb8(61, 123, 230), preferred),
|
||||
Color::BLACK
|
||||
);
|
||||
assert_eq!(foreground(Color::from_rgb8(61, 123, 230), preferred), Color::BLACK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -170,12 +153,7 @@ mod tests {
|
||||
.build();
|
||||
apply(&mut native, preferred);
|
||||
let theme = cosmic::Theme::custom(std::sync::Arc::new(native));
|
||||
for state in [
|
||||
State::Active,
|
||||
State::Hovered,
|
||||
State::Pressed,
|
||||
State::Disabled,
|
||||
] {
|
||||
for state in [State::Active, State::Hovered, State::Pressed, State::Disabled] {
|
||||
let actual = accent_button_style(&theme, state, false);
|
||||
assert_eq!(actual.text_color, Some(preferred));
|
||||
assert_eq!(actual.icon_color, Some(preferred));
|
||||
@@ -223,20 +201,11 @@ mod tests {
|
||||
for fill in [component.base, component.hover, component.pressed] {
|
||||
assert!(contrast(fill.into(), component.on.into()) >= 4.5);
|
||||
}
|
||||
assert!(
|
||||
contrast(component.selected.into(), component.selected_text.into()) >= 4.5
|
||||
);
|
||||
assert!(
|
||||
contrast(component.disabled.into(), component.on_disabled.into()) >= 4.5
|
||||
);
|
||||
assert!(contrast(component.selected.into(), component.selected_text.into()) >= 4.5);
|
||||
assert!(contrast(component.disabled.into(), component.on_disabled.into()) >= 4.5);
|
||||
}
|
||||
let theme = cosmic::Theme::custom(std::sync::Arc::new(native));
|
||||
for state in [
|
||||
State::Active,
|
||||
State::Hovered,
|
||||
State::Pressed,
|
||||
State::Disabled,
|
||||
] {
|
||||
for state in [State::Active, State::Hovered, State::Pressed, State::Disabled] {
|
||||
let style = accent_button_style(&theme, state, false);
|
||||
let Some(Background::Color(fill)) = style.background else {
|
||||
panic!("No fill")
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use cosmic::iced::{Background, Border, Color, Vector};
|
||||
use cosmic::widget::{button, container};
|
||||
use cosmic::{theme, widget};
|
||||
|
||||
use crate::{Palette, mix};
|
||||
use cosmic::{
|
||||
iced::{Background, Border, Color, Vector},
|
||||
theme,
|
||||
widget::{self, button, container},
|
||||
};
|
||||
|
||||
pub fn surface(background: Color, radius: f32) -> theme::Container<'static> {
|
||||
theme::Container::custom(move |_| surface_appearance(background, radius))
|
||||
@@ -85,11 +84,7 @@ fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button
|
||||
icon_color: Some(on),
|
||||
border_radius: p.radius.min(9.).into(),
|
||||
border_width: if selected { 1. } else { 0. },
|
||||
border_color: if selected {
|
||||
p.accent
|
||||
} else {
|
||||
Color::TRANSPARENT
|
||||
},
|
||||
border_color: if selected { p.accent } else { Color::TRANSPARENT },
|
||||
outline_width: 0.,
|
||||
outline_color: Color::TRANSPARENT,
|
||||
overlay: None,
|
||||
@@ -136,11 +131,7 @@ pub fn authentication_input(palette: Palette) -> theme::TextInput {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lock_input(
|
||||
radius: f32,
|
||||
accent: cosmic::iced::Color,
|
||||
surface: cosmic::iced::Color,
|
||||
) -> theme::TextInput {
|
||||
pub fn lock_input(radius: f32, accent: cosmic::iced::Color, surface: cosmic::iced::Color) -> theme::TextInput {
|
||||
let appearance = move |focused: bool| widget::text_input::Appearance {
|
||||
background: cosmic::iced::Color::from_rgba(
|
||||
(surface.r + 1.) * 0.5,
|
||||
@@ -190,9 +181,7 @@ pub fn shell_button(
|
||||
a: if pressed {
|
||||
0.20 * opacity
|
||||
} else {
|
||||
((if selected { 0.14 } else { 0.0 })
|
||||
+ progress * if selected { 0.02 } else { 0.08 })
|
||||
* opacity
|
||||
((if selected { 0.14 } else { 0.0 }) + progress * if selected { 0.02 } else { 0.08 }) * opacity
|
||||
},
|
||||
..foreground
|
||||
})),
|
||||
@@ -211,12 +200,7 @@ pub fn surface_appearance(background: Color, radius: f32) -> container::Style {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notification_button(
|
||||
foreground: Color,
|
||||
hover: Color,
|
||||
radius: f32,
|
||||
filled: bool,
|
||||
) -> theme::Button {
|
||||
pub fn notification_button(foreground: Color, hover: Color, radius: f32, filled: bool) -> theme::Button {
|
||||
let style = move |active: bool| button::Style {
|
||||
text_color: Some(foreground),
|
||||
icon_color: Some(foreground),
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
//! Canonical SVG assets, sizing and semantic tint rules for native Ferese UIs.
|
||||
use cosmic::{
|
||||
iced::{Color, ContentFit, Length},
|
||||
theme,
|
||||
widget::icon,
|
||||
};
|
||||
use cosmic::iced::{Color, ContentFit, Length};
|
||||
use cosmic::theme;
|
||||
use cosmic::widget::icon;
|
||||
|
||||
pub const ARROW: &[u8] = include_bytes!("../assets/icons/arrow.svg");
|
||||
pub const BATTERY: &[u8] = include_bytes!("../assets/icons/battery.svg");
|
||||
@@ -18,8 +16,7 @@ pub const BATTERY_75: &[u8] = include_bytes!("../assets/icons/status/battery-75.
|
||||
pub const BATTERY_CHARGING: &[u8] = include_bytes!("../assets/icons/status/battery-charging.svg");
|
||||
pub const BATTERY_EMPTY: &[u8] = include_bytes!("../assets/icons/status/battery-empty.svg");
|
||||
pub const BATTERY_FULL: &[u8] = include_bytes!("../assets/icons/status/battery-full.svg");
|
||||
pub const BLUETOOTH_CONNECTED: &[u8] =
|
||||
include_bytes!("../assets/icons/status/bluetooth-connected.svg");
|
||||
pub const BLUETOOTH_CONNECTED: &[u8] = include_bytes!("../assets/icons/status/bluetooth-connected.svg");
|
||||
pub const BLUETOOTH_OFF: &[u8] = include_bytes!("../assets/icons/status/bluetooth-off.svg");
|
||||
pub const BLUETOOTH_ON: &[u8] = include_bytes!("../assets/icons/status/bluetooth-on.svg");
|
||||
pub const BRIGHTNESS: &[u8] = include_bytes!("../assets/icons/status/brightness.svg");
|
||||
@@ -67,10 +64,7 @@ pub fn tinted(source: &'static [u8], size: u16, foreground: Color) -> icon::Icon
|
||||
}
|
||||
|
||||
pub fn symbolic(source: &'static [u8], size: u16) -> icon::Icon {
|
||||
icon::from_svg_bytes(source)
|
||||
.symbolic(true)
|
||||
.icon()
|
||||
.size(size)
|
||||
icon::from_svg_bytes(source).symbolic(true).icon().size(size)
|
||||
}
|
||||
|
||||
pub fn accented(source: &'static [u8], size: u16, foreground: Color, accent: Color) -> icon::Icon {
|
||||
@@ -88,8 +82,8 @@ pub fn accented(source: &'static [u8], size: u16, foreground: Color, accent: Col
|
||||
.size(size)
|
||||
.width(Length::Fixed(f32::from(size) * aspect))
|
||||
.content_fit(ContentFit::Contain)
|
||||
.class(theme::Svg::custom(move |_| {
|
||||
cosmic::iced::widget::svg::Style { color: None }
|
||||
.class(theme::Svg::custom(move |_| cosmic::iced::widget::svg::Style {
|
||||
color: None,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ferese_protocols::material::v1::client::{
|
||||
ferese_material_manager_v1::FereseMaterialManagerV1,
|
||||
ferese_surface_material_v1::FereseSurfaceMaterialV1,
|
||||
};
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, Proxy, QueueHandle, delegate_noop,
|
||||
globals::{GlobalListContents, registry_queue_init},
|
||||
protocol::{wl_registry, wl_surface},
|
||||
};
|
||||
use ferese_protocols::material::v1::client::ferese_material_manager_v1::FereseMaterialManagerV1;
|
||||
use ferese_protocols::material::v1::client::ferese_surface_material_v1::FereseSurfaceMaterialV1;
|
||||
use wayland_client::globals::{GlobalListContents, registry_queue_init};
|
||||
use wayland_client::protocol::{wl_registry, wl_surface};
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop};
|
||||
|
||||
/// Keeps a compositor-owned dialog material attached for the window lifetime.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -37,9 +33,8 @@ impl ModalMaterial {
|
||||
return Err("Dialog materials require Wayland".into());
|
||||
};
|
||||
// Iced lends live native handles; this backend borrows its connection.
|
||||
let backend = unsafe {
|
||||
wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast())
|
||||
};
|
||||
let backend =
|
||||
unsafe { wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast()) };
|
||||
let connection = Connection::from_backend(backend);
|
||||
let id = unsafe {
|
||||
wayland_client::backend::ObjectId::from_ptr(
|
||||
@@ -48,8 +43,7 @@ impl ModalMaterial {
|
||||
)
|
||||
}
|
||||
.map_err(|error| error.to_string())?;
|
||||
let surface =
|
||||
wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?;
|
||||
let surface = wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?;
|
||||
let (globals, mut queue) =
|
||||
registry_queue_init::<MaterialState>(&connection).map_err(|error| error.to_string())?;
|
||||
let qh = queue.handle();
|
||||
@@ -63,9 +57,7 @@ impl ModalMaterial {
|
||||
material,
|
||||
};
|
||||
let mut state = MaterialState::default();
|
||||
queue
|
||||
.roundtrip(&mut state)
|
||||
.map_err(|error| error.to_string())?;
|
||||
queue.roundtrip(&mut state).map_err(|error| error.to_string())?;
|
||||
if !state.ready {
|
||||
return Err("Dialog material attachment was not acknowledged".into());
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
//! Shared menu layouts and paint; callers own actions and animation timing.
|
||||
use cosmic::{
|
||||
Element,
|
||||
iced::{Alignment, Color, Length},
|
||||
widget::{button, container, row as iced_row},
|
||||
};
|
||||
use cosmic::Element;
|
||||
use cosmic::iced::{Alignment, Color, Length};
|
||||
use cosmic::widget::{button, container, row as iced_row};
|
||||
|
||||
pub fn button<'a, M: Clone + 'a>(
|
||||
label: impl Into<std::borrow::Cow<'a, str>> + 'a,
|
||||
@@ -20,12 +18,9 @@ pub fn row<'a, M: 'a>(
|
||||
trailing: Element<'a, M>,
|
||||
font: cosmic::font::Font,
|
||||
) -> Element<'a, M> {
|
||||
iced_row![
|
||||
crate::text(label, font).width(Length::Fill).size(14),
|
||||
trailing
|
||||
]
|
||||
.align_y(Alignment::Center)
|
||||
.into()
|
||||
iced_row![crate::text(label, font).width(Length::Fill).size(14), trailing]
|
||||
.align_y(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn heading<'a, M: 'a>(
|
||||
@@ -38,12 +33,7 @@ pub fn heading<'a, M: 'a>(
|
||||
.align_y(Alignment::Center)
|
||||
}
|
||||
|
||||
pub fn badge<'a, M: 'a>(
|
||||
content: Element<'a, M>,
|
||||
accent: Color,
|
||||
opacity: f32,
|
||||
radius: f32,
|
||||
) -> Element<'a, M> {
|
||||
pub fn badge<'a, M: 'a>(content: Element<'a, M>, accent: Color, opacity: f32, radius: f32) -> Element<'a, M> {
|
||||
container(content)
|
||||
.width(36)
|
||||
.height(36)
|
||||
@@ -56,12 +46,7 @@ pub fn badge<'a, M: 'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn section<'a, M: 'a>(
|
||||
content: Element<'a, M>,
|
||||
foreground: Color,
|
||||
opacity: f32,
|
||||
radius: f32,
|
||||
) -> Element<'a, M> {
|
||||
pub fn section<'a, M: 'a>(content: Element<'a, M>, foreground: Color, opacity: f32, radius: f32) -> Element<'a, M> {
|
||||
container(content)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
@@ -87,10 +72,7 @@ pub fn section_label<'a>(
|
||||
|
||||
pub fn separator<'a, M: 'a>(foreground: Color, opacity: f32) -> Element<'a, M> {
|
||||
container(cosmic::widget::Space::new().width(Length::Fill).height(1))
|
||||
.class(crate::controls::surface(
|
||||
foreground.scale_alpha(0.12 * opacity),
|
||||
0.,
|
||||
))
|
||||
.class(crate::controls::surface(foreground.scale_alpha(0.12 * opacity), 0.))
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -112,10 +94,7 @@ pub fn switch<'a, M: 'a>(enabled: bool, palette: crate::Palette, opacity: f32) -
|
||||
.width(16)
|
||||
.height(16)
|
||||
.class(crate::controls::surface(
|
||||
Color {
|
||||
a: opacity,
|
||||
..thumb
|
||||
},
|
||||
Color { a: opacity, ..thumb },
|
||||
palette.radius.min(8.),
|
||||
));
|
||||
container(knob)
|
||||
|
||||
@@ -102,11 +102,7 @@ pub fn parse_color(value: &str) -> Option<Color> {
|
||||
}
|
||||
|
||||
pub fn mix(a: Color, b: Color, t: f32) -> Color {
|
||||
Color::from_rgb(
|
||||
a.r + (b.r - a.r) * t,
|
||||
a.g + (b.g - a.g) * t,
|
||||
a.b + (b.b - a.b) * t,
|
||||
)
|
||||
Color::from_rgb(a.r + (b.r - a.r) * t, a.g + (b.g - a.g) * t, a.b + (b.b - a.b) * t)
|
||||
}
|
||||
|
||||
pub fn surface_shade(base: Color) -> Color {
|
||||
@@ -179,10 +175,8 @@ mod tests {
|
||||
("translucent", 2., 1.),
|
||||
("translucent", -1., 0.),
|
||||
] {
|
||||
let document = Document::parse(&format!(
|
||||
"theme {{ material {{ style {style}; opacity {opacity}; }} }}"
|
||||
))
|
||||
.unwrap();
|
||||
let document =
|
||||
Document::parse(&format!("theme {{ material {{ style {style}; opacity {opacity}; }} }}")).unwrap();
|
||||
assert_eq!(material_opacity(Some(&document)), expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Mutex, OnceLock},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
pub fn font(family: Option<&str>) -> cosmic::font::Font {
|
||||
let Some(family) = family else {
|
||||
|
||||
Reference in new issue
Block a user