diff --git a/api/lua/pinnacle/layout.lua b/api/lua/pinnacle/layout.lua index 19a7569..dcfbc74 100644 --- a/api/lua/pinnacle/layout.lua +++ b/api/lua/pinnacle/layout.lua @@ -283,11 +283,11 @@ function builtins.master_stack:layout(args) if self.master_side == "left" or self.master_side == "right" then coord = master_rect.y - len = master_rect.height + len = master_rect.height // (master_slice_count + 1) axis = "horizontal" else coord = master_rect.x - len = master_rect.width + len = master_rect.width // (master_slice_count + 1) axis = "vertical" end @@ -680,23 +680,21 @@ function builtins.fair:layout(args) table.insert(geos, rect) elseif win_count == 2 then local len + local coord if self.direction == "vertical" then len = rect.width + coord = rect.x else len = rect.height + coord = rect.y end -- Two windows is special cased to create a new line rather than increase to 2 in a line - local rect1, rect2 = rect:split_at(self.direction, len // 2 - gaps // 2, gaps) + local rect1, rect2 = rect:split_at(self.direction, coord + len // 2 - gaps // 2, gaps) if rect1 and rect2 then table.insert(geos, rect1) table.insert(geos, rect2) end else - -- 3 / 1 - -- 7 / 2 - -- 13 / 3 - -- 21 / 4 - local line_count = math.floor(math.sqrt(win_count) + 0.5) local wins_per_line = {} local max_per_line = line_count diff --git a/api/rust/examples/default_config/main.rs b/api/rust/examples/default_config/main.rs index e9aa9b7..13fe0e8 100644 --- a/api/rust/examples/default_config/main.rs +++ b/api/rust/examples/default_config/main.rs @@ -1,3 +1,6 @@ +use pinnacle_api::layout::{ + CornerLayout, DwindleLayout, FairLayout, LayoutManager, MasterStackLayout, SpiralLayout, +}; use pinnacle_api::signal::WindowSignal; use pinnacle_api::xkbcommon::xkb::Keysym; use pinnacle_api::{ @@ -15,6 +18,7 @@ async fn main() { input, output, tag, + layout, } = modules; let mod_key = Mod::Ctrl; @@ -78,6 +82,54 @@ async fn main() { // You can define window rules to get windows to open with desired properties. // See `pinnacle_api::window::rules` in the docs for more information. + // Layouts + + let master_stack = Box::::default(); + let dwindle = Box::::default(); + let spiral = Box::::default(); + let corner = Box::::default(); + let fair = Box::::default(); + + let layout_requester = layout.set_manager(layout.new_cycling_manager([ + master_stack as _, + dwindle as _, + spiral as _, + corner as _, + fair as _, + ])); + + let mut layout_requester_clone = layout_requester.clone(); + + // `mod_key + space` cycles to the next layout + input.keybind([mod_key], Keysym::space, move || { + let Some(focused_op) = output.get_focused() else { return }; + let Some(first_active_tag) = focused_op + .tags() + .into_iter() + .find(|tg| tg.active().unwrap_or(false)) + else { + return; + }; + + layout_requester.cycle_layout_forward(&first_active_tag); + layout_requester.request_layout_on_output(&focused_op); + }); + + // `mod_key + shift + space` cycles to the previous layout + input.keybind([mod_key, Mod::Shift], Keysym::space, move || { + let Some(focused_op) = output.get_focused() else { return }; + let Some(first_active_tag) = focused_op + .tags() + .into_iter() + .find(|tg| tg.active().unwrap_or(false)) + else { + return; + }; + + layout_requester_clone.cycle_layout_backward(&first_active_tag); + layout_requester_clone.request_layout_on_output(&focused_op); + }); + // Tags let tag_names = ["1", "2", "3", "4", "5"]; @@ -92,30 +144,6 @@ async fn main() { process.spawn_once([terminal]); - // Create a layout cycler to cycle through the given layouts - let LayoutCycler { - prev: layout_prev, - next: layout_next, - } = tag.new_layout_cycler([ - Layout::MasterStack, - Layout::Dwindle, - Layout::Spiral, - Layout::CornerTopLeft, - Layout::CornerTopRight, - Layout::CornerBottomLeft, - Layout::CornerBottomRight, - ]); - - // `mod_key + space` cycles to the next layout - input.keybind([mod_key], Keysym::space, move || { - layout_next(None); - }); - - // `mod_key + shift + space` cycles to the previous layout - input.keybind([mod_key, Mod::Shift], Keysym::space, move || { - layout_prev(None); - }); - for tag_name in tag_names { // `mod_key + 1-5` switches to tag "1" to "5" input.keybind([mod_key], tag_name, move || { diff --git a/api/rust/src/layout.rs b/api/rust/src/layout.rs new file mode 100644 index 0000000..115b8d8 --- /dev/null +++ b/api/rust/src/layout.rs @@ -0,0 +1,995 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Layout management. +//! +//! TODO: + +#![allow(missing_docs)] // TODO: + +use std::{collections::HashMap, sync::Arc}; + +use pinnacle_api_defs::pinnacle::layout::v0alpha1::{ + layout_request::{Body, ExplicitLayout, Geometries}, + layout_service_client::LayoutServiceClient, + LayoutRequest, +}; +use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; +use tokio_stream::StreamExt; +use tonic::transport::Channel; + +use crate::{ + block_on_tokio, + output::OutputHandle, + tag::TagHandle, + util::{Axis, Geometry}, + window::WindowHandle, + OUTPUT, TAG, WINDOW, +}; + +/// A struct that allows you to add and remove tags and get [`TagHandle`]s. +#[derive(Clone, Debug)] +pub struct Layout { + layout_client: LayoutServiceClient, +} + +impl Layout { + pub(crate) fn new(channel: Channel) -> Self { + Self { + layout_client: LayoutServiceClient::new(channel.clone()), + } + } + + pub fn new_cycling_manager( + &self, + layouts: impl IntoIterator>, + ) -> CyclingLayoutManager { + CyclingLayoutManager { + layouts: layouts.into_iter().collect(), + tag_indices: HashMap::new(), + } + } + + pub fn set_manager(&self, manager: M) -> LayoutRequester + where + M: LayoutManager + Send + 'static, + { + let (from_client, to_server) = unbounded_channel::(); + let to_server_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(to_server); + let mut from_server = block_on_tokio(self.layout_client.clone().layout(to_server_stream)) + .expect("TODO") + .into_inner(); + + let from_client_clone = from_client.clone(); + + let manager = Arc::new(tokio::sync::Mutex::new(manager)); + + let requester = LayoutRequester { + sender: from_client_clone, + manager: manager.clone(), + }; + + let thing = async move { + while let Some(Ok(response)) = from_server.next().await { + let args = LayoutArgs { + output: OUTPUT.get().unwrap().new_handle(response.output_name()), + windows: response + .window_ids + .into_iter() + .map(|id| WINDOW.get().unwrap().new_handle(id)) + .collect(), + tags: response + .tag_ids + .into_iter() + .map(|id| TAG.get().unwrap().new_handle(id)) + .collect(), + output_width: response.output_width.unwrap_or_default(), + output_height: response.output_height.unwrap_or_default(), + }; + let geos = manager.lock().await.active_layout(&args).layout(&args); + from_client + .send(LayoutRequest { + body: Some(Body::Geometries(Geometries { + request_id: response.request_id, + output_name: response.output_name, + geometries: geos + .into_iter() + .map(|geo| pinnacle_api_defs::pinnacle::v0alpha1::Geometry { + x: Some(geo.x), + y: Some(geo.y), + width: Some(geo.width as i32), + height: Some(geo.height as i32), + }) + .collect(), + })), + }) + .unwrap(); + } + }; + + tokio::spawn(thing); + requester + } +} + +#[derive(Clone, Debug)] +pub struct LayoutArgs { + pub output: OutputHandle, + pub windows: Vec, + pub tags: Vec, + pub output_width: u32, + pub output_height: u32, +} + +pub trait LayoutManager { + fn active_layout(&mut self, args: &LayoutArgs) -> &dyn LayoutGenerator; +} + +pub trait LayoutGenerator { + fn layout(&self, args: &LayoutArgs) -> Vec; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Gaps { + Absolute(u32), + Split { inner: u32, outer: u32 }, +} + +pub struct CyclingLayoutManager { + layouts: Vec>, + tag_indices: HashMap, +} + +impl CyclingLayoutManager { + pub fn new( + layout: &Layout, + layouts: impl IntoIterator>, + ) -> Self { + layout.new_cycling_manager(layouts) + } + + pub fn cycle_layout_forward(&mut self, tag: &TagHandle) { + let index = self.tag_indices.entry(tag.id).or_default(); + *index += 1; + if *index >= self.layouts.len() { + *index = 0; + } + } + + pub fn cycle_layout_backward(&mut self, tag: &TagHandle) { + let index = self.tag_indices.entry(tag.id).or_default(); + if let Some(i) = index.checked_sub(1) { + *index = i; + } else { + *index = self.layouts.len().saturating_sub(1); + } + } +} + +impl LayoutManager for CyclingLayoutManager { + fn active_layout(&mut self, args: &LayoutArgs) -> &dyn LayoutGenerator { + let Some(first_tag) = args.tags.first() else { + return &NoopLayout; + }; + + self.layouts + .get(*self.tag_indices.entry(first_tag.id).or_default()) + .expect("no layouts in manager") + .as_ref() + } +} + +#[derive(Debug)] +pub struct LayoutRequester { + sender: UnboundedSender, + pub manager: Arc>, +} + +impl Clone for LayoutRequester { + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + manager: self.manager.clone(), + } + } +} + +impl LayoutRequester { + pub fn request_layout(&self) { + let output_name = OUTPUT.get().unwrap().get_focused().map(|op| op.name); + self.sender + .send(LayoutRequest { + body: Some(Body::Layout(ExplicitLayout { output_name })), + }) + .unwrap(); + } + + pub fn request_layout_on_output(&self, output: &OutputHandle) { + self.sender + .send(LayoutRequest { + body: Some(Body::Layout(ExplicitLayout { + output_name: Some(output.name.clone()), + })), + }) + .unwrap(); + } +} + +impl LayoutRequester { + pub fn cycle_layout_forward(&self, tag: &TagHandle) { + let mut lock = block_on_tokio(self.manager.lock()); + lock.cycle_layout_forward(tag); + } + + pub fn cycle_layout_backward(&mut self, tag: &TagHandle) { + let mut lock = block_on_tokio(self.manager.lock()); + lock.cycle_layout_backward(tag); + } +} + +pub struct NoopLayout; + +impl LayoutGenerator for NoopLayout { + fn layout(&self, _args: &LayoutArgs) -> Vec { + Vec::new() + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum MasterSide { + Left, + Right, + Top, + Bottom, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct MasterStackLayout { + pub gaps: Gaps, + pub master_factor: f32, + pub master_side: MasterSide, + pub master_count: u32, +} + +impl Default for MasterStackLayout { + fn default() -> Self { + Self { + gaps: Gaps::Absolute(8), + master_factor: 0.5, + master_side: MasterSide::Left, + master_count: 1, + } + } +} + +impl LayoutGenerator for MasterStackLayout { + fn layout(&self, args: &LayoutArgs) -> Vec { + let win_count = args.windows.len() as u32; + + if win_count == 0 { + return Vec::new(); + } + + let width = args.output_width; + let height = args.output_height; + + let mut geos = Vec::::new(); + + let (outer_gaps, inner_gaps) = match self.gaps { + Gaps::Absolute(gaps) => (gaps, None), + Gaps::Split { inner, outer } => (outer, Some(inner)), + }; + + let rect = Geometry { + x: 0, + y: 0, + width, + height, + } + .split_at(Axis::Horizontal, 0, outer_gaps) + .0 + .split_at(Axis::Horizontal, (height - outer_gaps) as i32, outer_gaps) + .0 + .split_at(Axis::Vertical, 0, outer_gaps) + .0 + .split_at(Axis::Vertical, (width - outer_gaps) as i32, outer_gaps) + .0; + + let master_factor = if win_count > self.master_count { + self.master_factor.clamp(0.1, 0.9) + } else { + 1.0 + }; + + let gaps = match inner_gaps { + Some(_) => 0, + None => outer_gaps, + }; + + let (master_rect, mut stack_rect) = match self.master_side { + MasterSide::Left => { + let (rect1, rect2) = rect.split_at( + Axis::Vertical, + (width as f32 * master_factor).floor() as i32 - gaps as i32 / 2, + gaps, + ); + (Some(rect1), rect2) + } + MasterSide::Right => { + let (rect2, rect1) = rect.split_at( + Axis::Vertical, + (width as f32 * master_factor).floor() as i32 - gaps as i32 / 2, + gaps, + ); + (rect1, Some(rect2)) + } + MasterSide::Top => { + let (rect1, rect2) = rect.split_at( + Axis::Horizontal, + (height as f32 * master_factor).floor() as i32 - gaps as i32 / 2, + gaps, + ); + (Some(rect1), rect2) + } + MasterSide::Bottom => { + let (rect2, rect1) = rect.split_at( + Axis::Horizontal, + (height as f32 * master_factor).floor() as i32 - gaps as i32 / 2, + gaps, + ); + (rect1, Some(rect2)) + } + }; + + let mut master_rect = master_rect.unwrap_or_else(|| stack_rect.take().unwrap()); + + let (master_count, stack_count) = if win_count > self.master_count { + (self.master_count, Some(win_count - self.master_count)) + } else { + (win_count, None) + }; + + if master_count > 1 { + let (coord, len, axis) = match self.master_side { + MasterSide::Left | MasterSide::Right => ( + master_rect.y, + master_rect.height as f32 / master_count as f32, + Axis::Horizontal, + ), + MasterSide::Top | MasterSide::Bottom => ( + master_rect.x, + master_rect.width as f32 / master_count as f32, + Axis::Vertical, + ), + }; + + for i in 1..master_count { + let slice_point = coord + (len * i as f32) as i32 - gaps as i32 / 2; + let (to_push, rest) = master_rect.split_at(axis, slice_point, gaps); + geos.push(to_push); + if let Some(rest) = rest { + master_rect = rest; + } else { + break; + } + } + } + + geos.push(master_rect); + + if let Some(stack_count) = stack_count { + let mut stack_rect = stack_rect.unwrap(); + + if stack_count > 1 { + let (coord, len, axis) = match self.master_side { + MasterSide::Left | MasterSide::Right => ( + stack_rect.y, + stack_rect.height as f32 / stack_count as f32, + Axis::Horizontal, + ), + MasterSide::Top | MasterSide::Bottom => ( + stack_rect.x, + stack_rect.width as f32 / stack_count as f32, + Axis::Vertical, + ), + }; + + for i in 1..stack_count { + let slice_point = coord + (len * i as f32) as i32 - gaps as i32 / 2; + let (to_push, rest) = stack_rect.split_at(axis, slice_point, gaps); + geos.push(to_push); + if let Some(rest) = rest { + stack_rect = rest; + } else { + break; + } + } + } + + geos.push(stack_rect); + } + + if let Some(inner_gaps) = inner_gaps { + for geo in geos.iter_mut() { + geo.x += inner_gaps as i32; + geo.y += inner_gaps as i32; + geo.width -= inner_gaps * 2; + geo.height -= inner_gaps * 2; + } + } + + geos + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct DwindleLayout { + pub gaps: Gaps, + pub split_factors: HashMap, +} + +impl Default for DwindleLayout { + fn default() -> Self { + Self { + gaps: Gaps::Absolute(8), + split_factors: Default::default(), + } + } +} + +impl LayoutGenerator for DwindleLayout { + fn layout(&self, args: &LayoutArgs) -> Vec { + let win_count = args.windows.len() as u32; + + if win_count == 0 { + return Vec::new(); + } + + let width = args.output_width; + let height = args.output_height; + + let mut geos = Vec::::new(); + + let (outer_gaps, inner_gaps) = match self.gaps { + Gaps::Absolute(gaps) => (gaps, None), + Gaps::Split { inner, outer } => (outer, Some(inner)), + }; + + let gaps = match inner_gaps { + Some(_) => 0, + None => outer_gaps, + }; + + let mut rect = Geometry { + x: 0, + y: 0, + width, + height, + } + .split_at(Axis::Horizontal, 0, outer_gaps) + .0 + .split_at(Axis::Horizontal, (height - outer_gaps) as i32, outer_gaps) + .0 + .split_at(Axis::Vertical, 0, outer_gaps) + .0 + .split_at(Axis::Vertical, (width - outer_gaps) as i32, outer_gaps) + .0; + + if win_count == 1 { + geos.push(rect) + } else { + for i in 1..win_count { + let factor = self + .split_factors + .get(&(i as usize)) + .copied() + .unwrap_or(0.5) + .clamp(0.1, 0.9); + + let (axis, mut split_coord) = if i % 2 == 1 { + (Axis::Vertical, rect.x + (rect.width as f32 * factor) as i32) + } else { + ( + Axis::Horizontal, + rect.y + (rect.height as f32 * factor) as i32, + ) + }; + split_coord -= gaps as i32 / 2; + + let (to_push, rest) = rect.split_at(axis, split_coord, gaps); + + geos.push(to_push); + + if let Some(rest) = rest { + rect = rest; + } else { + break; + } + } + + geos.push(rect) + } + + if let Some(inner_gaps) = inner_gaps { + for geo in geos.iter_mut() { + geo.x += inner_gaps as i32; + geo.y += inner_gaps as i32; + geo.width -= inner_gaps * 2; + geo.height -= inner_gaps * 2; + } + } + + geos + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct SpiralLayout { + pub gaps: Gaps, + pub split_factors: HashMap, +} + +impl Default for SpiralLayout { + fn default() -> Self { + Self { + gaps: Gaps::Absolute(8), + split_factors: Default::default(), + } + } +} + +impl LayoutGenerator for SpiralLayout { + fn layout(&self, args: &LayoutArgs) -> Vec { + let win_count = args.windows.len() as u32; + + if win_count == 0 { + return Vec::new(); + } + + let width = args.output_width; + let height = args.output_height; + + let mut geos = Vec::::new(); + + let (outer_gaps, inner_gaps) = match self.gaps { + Gaps::Absolute(gaps) => (gaps, None), + Gaps::Split { inner, outer } => (outer, Some(inner)), + }; + + let gaps = match inner_gaps { + Some(_) => 0, + None => outer_gaps, + }; + + let mut rect = Geometry { + x: 0, + y: 0, + width, + height, + } + .split_at(Axis::Horizontal, 0, outer_gaps) + .0 + .split_at(Axis::Horizontal, (height - outer_gaps) as i32, outer_gaps) + .0 + .split_at(Axis::Vertical, 0, outer_gaps) + .0 + .split_at(Axis::Vertical, (width - outer_gaps) as i32, outer_gaps) + .0; + + if win_count == 1 { + geos.push(rect) + } else { + for i in 1..win_count { + let factor = self + .split_factors + .get(&(i as usize)) + .copied() + .unwrap_or(0.5) + .clamp(0.1, 0.9); + + let (axis, mut split_coord) = if i % 2 == 1 { + (Axis::Vertical, rect.x + (rect.width as f32 * factor) as i32) + } else { + ( + Axis::Horizontal, + rect.y + (rect.height as f32 * factor) as i32, + ) + }; + split_coord -= gaps as i32 / 2; + + let (to_push, rest) = if let 1 | 2 = i % 4 { + let (to_push, rest) = rect.split_at(axis, split_coord, gaps); + (Some(to_push), rest) + } else { + let (rest, to_push) = rect.split_at(axis, split_coord, gaps); + (to_push, Some(rest)) + }; + + if let Some(to_push) = to_push { + geos.push(to_push); + } + + if let Some(rest) = rest { + rect = rest; + } else { + break; + } + } + + geos.push(rect) + } + + if let Some(inner_gaps) = inner_gaps { + for geo in geos.iter_mut() { + geo.x += inner_gaps as i32; + geo.y += inner_gaps as i32; + geo.width -= inner_gaps * 2; + geo.height -= inner_gaps * 2; + } + } + + geos + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CornerLocation { + TopLeft, + TopRight, + BottomLeft, + BottomRight, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct CornerLayout { + pub gaps: Gaps, + pub corner_width_factor: f32, + pub corner_height_factor: f32, + pub corner_loc: CornerLocation, +} + +impl Default for CornerLayout { + fn default() -> Self { + Self { + gaps: Gaps::Absolute(8), + corner_width_factor: 0.5, + corner_height_factor: 0.5, + corner_loc: CornerLocation::TopLeft, + } + } +} + +impl LayoutGenerator for CornerLayout { + fn layout(&self, args: &LayoutArgs) -> Vec { + let win_count = args.windows.len() as u32; + + if win_count == 0 { + return Vec::new(); + } + + let width = args.output_width; + let height = args.output_height; + + let mut geos = Vec::::new(); + + let (outer_gaps, inner_gaps) = match self.gaps { + Gaps::Absolute(gaps) => (gaps, None), + Gaps::Split { inner, outer } => (outer, Some(inner)), + }; + + let gaps = match inner_gaps { + Some(_) => 0, + None => outer_gaps, + }; + + let rect = Geometry { + x: 0, + y: 0, + width, + height, + } + .split_at(Axis::Horizontal, 0, outer_gaps) + .0 + .split_at(Axis::Horizontal, (height - outer_gaps) as i32, outer_gaps) + .0 + .split_at(Axis::Vertical, 0, outer_gaps) + .0 + .split_at(Axis::Vertical, (width - outer_gaps) as i32, outer_gaps) + .0; + + if win_count == 1 { + geos.push(rect) + } else { + let (mut corner_rect, vert_stack_rect) = match self.corner_loc { + CornerLocation::TopLeft | CornerLocation::BottomLeft => { + let x_slice_point = rect.x + + (rect.width as f32 * self.corner_width_factor).round() as i32 + - gaps as i32 / 2; + let (corner_rect, vert_stack_rect) = + rect.split_at(Axis::Vertical, x_slice_point, gaps); + (Some(corner_rect), vert_stack_rect) + } + CornerLocation::TopRight | CornerLocation::BottomRight => { + let x_slice_point = rect.x + + (rect.width as f32 * (1.0 - self.corner_width_factor)).round() as i32 + - gaps as i32 / 2; + let (vert_stack_rect, corner_rect) = + rect.split_at(Axis::Vertical, x_slice_point, gaps); + (corner_rect, Some(vert_stack_rect)) + } + }; + + if win_count == 2 { + geos.extend([corner_rect, vert_stack_rect].into_iter().flatten()); + } else { + let horiz_stack_rect = match self.corner_loc { + CornerLocation::TopLeft | CornerLocation::TopRight => { + let y_slice_point = rect.y + + (rect.height as f32 * self.corner_height_factor).round() as i32 + - gaps as i32 / 2; + + corner_rect.and_then(|corner| { + let (corner, horiz) = + corner.split_at(Axis::Horizontal, y_slice_point, gaps); + corner_rect = Some(corner); + horiz + }) + } + CornerLocation::BottomLeft | CornerLocation::BottomRight => { + let y_slice_point = rect.y + + (rect.height as f32 * (1.0 - self.corner_height_factor)).round() + as i32 + - gaps as i32 / 2; + + corner_rect.map(|corner| { + let (horiz, corner) = + corner.split_at(Axis::Horizontal, y_slice_point, gaps); + corner_rect = corner; + horiz + }) + } + }; + + if let (Some(mut horiz_stack_rect), Some(mut vert_stack_rect), Some(corner_rect)) = + (horiz_stack_rect, vert_stack_rect, corner_rect) + { + geos.push(corner_rect); + + let mut vert_geos = Vec::new(); + let mut horiz_geos = Vec::new(); + + let vert_stack_count = ((win_count - 1) as f32 / 2.0).ceil() as i32; + let horiz_stack_count = ((win_count - 1) as f32 / 2.0).floor() as i32; + + let vert_stack_y = vert_stack_rect.y; + let vert_win_height = vert_stack_rect.height as f32 / vert_stack_count as f32; + + for i in 1..vert_stack_count { + let slice_point = vert_stack_y + + (vert_win_height * i as f32).round() as i32 + - gaps as i32 / 2; + + let (to_push, rest) = + vert_stack_rect.split_at(Axis::Horizontal, slice_point, gaps); + + vert_geos.push(to_push); + + if let Some(rest) = rest { + vert_stack_rect = rest; + } else { + break; + } + } + + vert_geos.push(vert_stack_rect); + + let horiz_stack_x = horiz_stack_rect.x; + let horiz_win_width = horiz_stack_rect.width as f32 / horiz_stack_count as f32; + + for i in 1..horiz_stack_count { + let slice_point = horiz_stack_x + + (horiz_win_width * i as f32).round() as i32 + - gaps as i32 / 2; + + let (to_push, rest) = + horiz_stack_rect.split_at(Axis::Vertical, slice_point, gaps); + + horiz_geos.push(to_push); + + if let Some(rest) = rest { + horiz_stack_rect = rest; + } else { + break; + } + } + + horiz_geos.push(horiz_stack_rect); + + for i in 0..(vert_geos.len() + horiz_geos.len()) { + if i % 2 == 0 { + geos.push(vert_geos[i / 2]); + } else { + geos.push(horiz_geos[i / 2]); + } + } + } + } + } + + if let Some(inner_gaps) = inner_gaps { + for geo in geos.iter_mut() { + geo.x += inner_gaps as i32; + geo.y += inner_gaps as i32; + geo.width -= inner_gaps * 2; + geo.height -= inner_gaps * 2; + } + } + + geos + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct FairLayout { + pub gaps: Gaps, + pub axis: Axis, +} + +impl Default for FairLayout { + fn default() -> Self { + Self { + gaps: Gaps::Absolute(8), + axis: Axis::Vertical, + } + } +} + +impl LayoutGenerator for FairLayout { + fn layout(&self, args: &LayoutArgs) -> Vec { + let win_count = args.windows.len() as u32; + + if win_count == 0 { + return Vec::new(); + } + + let width = args.output_width; + let height = args.output_height; + + let mut geos = Vec::::new(); + + let (outer_gaps, inner_gaps) = match self.gaps { + Gaps::Absolute(gaps) => (gaps, None), + Gaps::Split { inner, outer } => (outer, Some(inner)), + }; + + let gaps = match inner_gaps { + Some(_) => 0, + None => outer_gaps, + }; + + let mut rect = Geometry { + x: 0, + y: 0, + width, + height, + } + .split_at(Axis::Horizontal, 0, outer_gaps) + .0 + .split_at(Axis::Horizontal, (height - outer_gaps) as i32, outer_gaps) + .0 + .split_at(Axis::Vertical, 0, outer_gaps) + .0 + .split_at(Axis::Vertical, (width - outer_gaps) as i32, outer_gaps) + .0; + + if win_count == 1 { + geos.push(rect); + } else if win_count == 2 { + let len = match self.axis { + Axis::Vertical => rect.width, + Axis::Horizontal => rect.height, + }; + + let coord = match self.axis { + Axis::Vertical => rect.x, + Axis::Horizontal => rect.y, + }; + + let (rect1, rect2) = + rect.split_at(self.axis, coord + len as i32 / 2 - gaps as i32 / 2, gaps); + + geos.push(rect1); + if let Some(rect2) = rect2 { + geos.push(rect2); + } + } else { + let line_count = (win_count as f32).sqrt().round() as u32; + + let mut wins_per_line = Vec::new(); + + let max_per_line = if win_count > line_count * line_count { + line_count + 1 + } else { + line_count + }; + + for i in 1..=win_count { + let index = (i as f32 / max_per_line as f32).ceil() as usize - 1; + if wins_per_line.get(index).is_none() { + wins_per_line.push(0); + } + wins_per_line[index] += 1; + } + + assert_eq!(wins_per_line.len(), line_count as usize); + + let mut line_rects = Vec::new(); + + let (coord, len, axis) = match self.axis { + Axis::Horizontal => ( + rect.y, + rect.height as f32 / line_count as f32, + Axis::Horizontal, + ), + Axis::Vertical => ( + rect.x, + rect.width as f32 / line_count as f32, + Axis::Vertical, + ), + }; + + for i in 1..line_count { + let slice_point = coord + (len * i as f32) as i32 - gaps as i32 / 2; + let (to_push, rest) = rect.split_at(axis, slice_point, gaps); + line_rects.push(to_push); + if let Some(rest) = rest { + rect = rest; + } else { + break; + } + } + + line_rects.push(rect); + + for (i, mut line_rect) in line_rects.into_iter().enumerate() { + let (coord, len, axis) = match self.axis { + Axis::Vertical => ( + line_rect.y, + line_rect.height as f32 / wins_per_line[i] as f32, + Axis::Horizontal, + ), + Axis::Horizontal => ( + line_rect.x, + line_rect.width as f32 / wins_per_line[i] as f32, + Axis::Vertical, + ), + }; + + for j in 1..wins_per_line[i] { + let slice_point = coord + (len * j as f32) as i32 - gaps as i32 / 2; + let (to_push, rest) = line_rect.split_at(axis, slice_point, gaps); + geos.push(to_push); + if let Some(rest) = rest { + line_rect = rest; + } else { + break; + } + } + + geos.push(line_rect); + } + } + + if let Some(inner_gaps) = inner_gaps { + for geo in geos.iter_mut() { + geo.x += inner_gaps as i32; + geo.y += inner_gaps as i32; + geo.width -= inner_gaps * 2; + geo.height -= inner_gaps * 2; + } + } + + geos + } +} diff --git a/api/rust/src/lib.rs b/api/rust/src/lib.rs index 728ecd5..26068e1 100644 --- a/api/rust/src/lib.rs +++ b/api/rust/src/lib.rs @@ -2,6 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +#![deny(elided_lifetimes_in_paths)] #![warn(missing_docs)] //! The Rust implementation of [Pinnacle](https://github.com/pinnacle-comp/pinnacle)'s @@ -87,6 +88,7 @@ use futures::{ Future, StreamExt, }; use input::Input; +use layout::Layout; use output::Output; use pinnacle::Pinnacle; use process::Process; @@ -102,6 +104,7 @@ use tower::service_fn; use window::Window; pub mod input; +pub mod layout; pub mod output; pub mod pinnacle; pub mod process; @@ -121,6 +124,7 @@ static INPUT: OnceLock = OnceLock::new(); static OUTPUT: OnceLock = OnceLock::new(); static TAG: OnceLock = OnceLock::new(); static SIGNAL: OnceLock> = OnceLock::new(); +static LAYOUT: OnceLock = OnceLock::new(); /// A struct containing static references to all of the configuration structs. #[derive(Debug, Clone, Copy)] @@ -137,6 +141,8 @@ pub struct ApiModules { pub output: &'static Output, /// The [`Tag`] struct pub tag: &'static Tag, + /// The [`Layout`] struct + pub layout: &'static Layout, } /// Connects to Pinnacle and builds the configuration structs. @@ -154,7 +160,7 @@ pub async fn connect( })) .await?; - let (fut_sender, fut_recv) = unbounded_channel::>(); + let (fut_sender, fut_recv) = unbounded_channel::>(); let pinnacle = PINNACLE.get_or_init(|| Pinnacle::new(channel.clone())); let process = PROCESS.get_or_init(|| Process::new(channel.clone(), fut_sender.clone())); @@ -162,6 +168,7 @@ pub async fn connect( let input = INPUT.get_or_init(|| Input::new(channel.clone(), fut_sender.clone())); let tag = TAG.get_or_init(|| Tag::new(channel.clone())); let output = OUTPUT.get_or_init(|| Output::new(channel.clone())); + let layout = LAYOUT.get_or_init(|| Layout::new(channel.clone())); SIGNAL .set(RwLock::new(SignalState::new( @@ -177,6 +184,7 @@ pub async fn connect( input, output, tag, + layout, }; Ok((modules, fut_recv)) @@ -190,7 +198,7 @@ pub async fn connect( /// This function is inserted at the end of your config through the [`config`] macro. /// You should use the macro instead of this function directly. pub async fn listen(fut_recv: UnboundedReceiver>) { - let mut future_set = FuturesUnordered::>::new(); + let mut future_set = FuturesUnordered::>::new(); let mut fut_recv = UnboundedReceiverStream::new(fut_recv); diff --git a/api/rust/src/signal.rs b/api/rust/src/signal.rs index 074a167..59e2232 100644 --- a/api/rust/src/signal.rs +++ b/api/rust/src/signal.rs @@ -313,7 +313,6 @@ where } } _dc = dc_ping_recv_fuse => { - println!("dc"); control_sender.send(Req::from_control(StreamControl::Disconnect)).expect("send failed"); break; } diff --git a/api/rust/src/util.rs b/api/rust/src/util.rs index db02c23..250591c 100644 --- a/api/rust/src/util.rs +++ b/api/rust/src/util.rs @@ -26,6 +26,97 @@ pub struct Geometry { pub height: u32, } +#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] +pub enum Axis { + Horizontal, + Vertical, +} + +impl Geometry { + pub fn split_at(mut self, axis: Axis, at: i32, thickness: u32) -> (Geometry, Option) { + match axis { + Axis::Horizontal => { + if at <= self.y { + let diff = at - self.y + thickness as i32; + if diff > 0 { + self.y += diff; + self.height = self.height.saturating_sub(diff as u32); + } + (self, None) + } else if at >= self.y + self.height as i32 { + (self, None) + } else if at + thickness as i32 >= self.y + self.height as i32 { + let diff = self.y + self.height as i32 - at; + self.height = self.height.saturating_sub(diff as u32); + (self, None) + } else { + let x = self.x; + let top_y = self.y; + let width = self.width; + let top_height = at - self.y; + + let bot_y = at + thickness as i32; + let bot_height = self.y + self.height as i32 - at - thickness as i32; + + let geo1 = Geometry { + x, + y: top_y, + width, + height: top_height as u32, + }; + let geo2 = Geometry { + x, + y: bot_y, + width, + height: bot_height as u32, + }; + + (geo1, Some(geo2)) + } + } + Axis::Vertical => { + if at <= self.x { + let diff = at - self.x + thickness as i32; + if diff > 0 { + self.x += diff; + self.width = self.width.saturating_sub(diff as u32); + } + (self, None) + } else if at >= self.x + self.width as i32 { + (self, None) + } else if at + thickness as i32 >= self.x + self.width as i32 { + let diff = self.x + self.width as i32 - at; + self.width = self.width.saturating_sub(diff as u32); + (self, None) + } else { + let left_x = self.x; + let y = self.y; + let left_width = at - self.x; + let height = self.height; + + let right_x = at + thickness as i32; + let right_width = self.x + self.width as i32 - at - thickness as i32; + + let geo1 = Geometry { + x: left_x, + y, + width: left_width as u32, + height, + }; + let geo2 = Geometry { + x: right_x, + y, + width: right_width as u32, + height, + }; + + (geo1, Some(geo2)) + } + } + } + } +} + /// Batch a set of requests that will be sent ot the compositor all at once. /// /// # Rationale