use super::TagId;
use crate::{layouts::Layout, Window, Workspace};
use serde::{Deserialize, Serialize};
/// Wrapper struct holding all the tags.
/// This wrapper provides convenience methods to change the tag-list
/// during its lifetime, while ensuring that all tags are in correct order
/// and numbered accordingly.
///
/// Each Tag is stored in either the 'normal' or the 'hidden' list.
/// This is just an internal simplification for handling the different
/// kinds of Tags.
///
/// ## Normal Tags
/// Normal tags are the tags visible to the user, those are the tags
/// that can be labelled via the config file. Tags are identified by a
/// unique ID which is automatically assigned by `LeftWM`, those IDs start at 1
/// and increment by 1. You can always expect that the tags are ordered by their ID
/// and that there is no gap between the numbers. The largest tag ID is equal to the
/// amount of normal tags. This also means: tags with larger IDs are always to the right
/// and tags with smaller IDs are always to the left of the reference tag.
///
/// Usually the number of normal tags is equal to the number of tags configured by the user.
/// However, if there are more workspaces than there are tags, additional "unnamed" tags
/// will be created automatically and appended to the list.
///
/// ## Hidden Tags
/// A hidden tag is a tag that is invisible and unknown to the user.
/// Those tags are created in the source code and can be used for
/// various purposes. The Scratchpad (NSP) feature is an example which uses
/// a hidden Tag called "NSP" to hide away the scratchpad until its summoned again.
/// You can think of a hidden tag as an invisible window storage. It is not possible
/// for a user to display a hidden tag in a workspace.
///
/// Hidden tags also have a unique ID starting at `usize::MAX`, decrementing by 1.
/// This prevents conflicts of Tag IDs between normal and hidden tags and makes it easier
/// to dynamically change normal tags and their ID without affecting hidden tags and vice versa.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Tags {
// holds all the 'normal' tags
normal: Vec<Tag>,
// holds the 'hidden' tags (like 'NSP')
hidden: Vec<Tag>,
}
impl Tags {
/// Create a new empty Taglist
pub const fn new() -> Self {
Self {
normal: vec![],
hidden: vec![],
}
}
/// Create a new tag with the provided label and layout,
/// and append it to the list of normal tags.
/// The ID will be assigned automatically and returned.
pub fn add_new(&mut self, label: &str, layout: Layout) -> TagId {
let next_id = self.normal.len() + 1; // tag id starts at 1
let tag = Tag::new(next_id, label, layout);
let id = tag.id;
self.normal.push(tag);
id
}
/// Create a new tag with the provided layout, labelling it directly with its ID,
/// and append it to the list of normal tags.
/// The ID will be assigned automatically and returned.
pub fn add_new_unlabeled(&mut self, layout: Layout) -> TagId {
let next_id = self.normal.len() + 1; // tag id starts at 1
self.add_new(next_id.to_string().as_str(), layout)
}
// todo: add_new_at(position, label, layout)
// -> shifting all one to the right and re-number them (vec.insert)
// todo: remove(id)
// -> shifting all right of the removed tag one to the left and re-number them
/// Create a new hidden tag with the provided label,
/// and append it to the list of hidden tags.
/// The ID will be assigned automatically and returned.
///
/// ## Non unique label
/// May not create a new tag if a hidden tag with the same label already exists,
/// this is indicated by the return value of `None`.
pub fn add_new_hidden(&mut self, label: &str) -> Option<TagId> {
if self.get_hidden_by_label(label).is_none() {
// hidden tags are numbered descending from the highest possible number
// to prevent conflicts with IDs of normal tags
let next_id = usize::MAX - self.hidden.len();
let tag = Tag {
id: next_id,
label: label.to_string(),
hidden: true,
..Tag::default()
};
let id = tag.id;
self.hidden.push(tag);
Some(id)
} else {
tracing::error!(
"tried creating a hidden tag with label {}, but a hidden tag with the same label already exists",
label
);
None
}
}
/// Get all normal tags
pub const fn normal(&self) -> &Vec<Tag> {
&self.normal
}
/// Get a list of all tags, including hidden ones.
/// The hidden tags are at the end of the list.
pub fn all(&self) -> Vec<&Tag> {
//&self.normal.append(&self.hidden)
let mut result: Vec<&Tag> = vec![];
self.normal.iter().for_each(|tag| result.push(tag));
self.hidden.iter().for_each(|tag| result.push(tag));
result
}
/// Get a list of all tags as mutable, including hidden ones.
/// The hidden tags are at the end of the list
pub fn all_mut(&mut self) -> Vec<&mut Tag> {
//&self.normal.append(&self.hidden)
let mut result: Vec<&mut Tag> = vec![];
self.normal.iter_mut().for_each(|tag| result.push(tag));
self.hidden.iter_mut().for_each(|tag| result.push(tag));
result
}
/// Get a tag by its ID.
/// This method returns normal, as well as hidden tags.
pub fn get(&self, id: TagId) -> Option<&Tag> {
self.normal
.get(id - 1) // tag id starts at 1, but arrays at 0 :)
.or_else(|| self.hidden.iter().find(|&hidden_tag| hidden_tag.id == id))
}
/// Get a tag by its ID as mutable
/// This method returns normal, as well as hidden tags.
pub fn get_mut(&mut self, id: TagId) -> Option<&mut Tag> {
if let Some(normal) = self.normal.get_mut(id - 1) {
return Some(normal);
}
return self
.hidden
.iter_mut()
.find(|hidden_tag| hidden_tag.id == id);
}
/// Get a hidden tag by its label
pub fn get_hidden_by_label(&self, label: &str) -> Option<&Tag> {
self.hidden.iter().find(|tag| tag.label.eq(label))
}
/// Get the amount of 'normal' tags
pub fn len_normal(&self) -> usize {
self.normal.len()
}
}
impl Default for Tags {
fn default() -> Self {
Self::new()
}
}
/// A Tag is similar to a "Desktop".
/// Each Screen/Workspace will always display a certain Tag.
/// A Tag can not be displayed on more than one Workspace at a time.
///
/// Unlike in some other WMs (eg. `dwm`), in `LeftWM`
/// the same set of tags and windows are shared among
/// all Workspaces, this means there aren't multiple instances of
/// the same Tag on different Screens.
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Tag {
/// Unique identifier for the tag,
/// this is automatically assigned by `LeftWM`.
/// IDs start at 1 for the first tag, and are
/// incremented by 1 for each subsequent tag.
pub id: TagId,
/// Label of the tag, only used for display purposes.
///
/// Labels of normal tags may not be unique,
/// but labels of hidden tags must be unique.
///
/// ## Hint
/// Unlike in earlier versions of LeftWM,
/// the label of a Tag is not something that
/// actually identifies a Tag. Tags are always
/// identified and referenced by their ID (ie. `[1, 2, 3, ...]`).
///
/// This means, if the user configures the tags `["home", "chat", "surf", "code"]`
/// and later removes the `chat` tag, leaving `["home", "surf", "code"]`, this does not mean
/// that Tag 2 is removed, it just means that Tag 2 has the label "surf" now.
/// What is actually removed in that case is Tag 4.
pub label: String,
/// Indicates whether the tag can be
/// displayed in a Workspace or not.
/// Hidden tags are internal only, and
/// are unknown to other programs (eg. polybar)
pub hidden: bool,
/// The layout in which the windows
/// on this Tag are arranged
pub layout: Layout,
/// The percentage of available space
/// which is designated for the "main"
/// column of the layout, compared
/// to the secondary column(s).
pub main_width_percentage: u8,
pub flipped_horizontal: bool,
pub flipped_vertical: bool,
pub layout_rotation: usize,
}
impl Tag {
#[must_use]
pub fn new(id: TagId, label: &str, layout: Layout) -> Self {
Self {
id,
label: label.to_owned(),
hidden: false,
layout,
main_width_percentage: layout.main_width(),
flipped_horizontal: false,
flipped_vertical: false,
layout_rotation: 0,
}
}
pub fn update_windows(&self, windows: &mut [Window], workspace: &Workspace) {
if let Some(window) = windows
.iter_mut()
.find(|w| w.has_tag(&self.id) && w.is_fullscreen())
{
window.set_visible(true);
window.normal = workspace.xyhw;
let handle = window.handle;
windows
.iter_mut()
.filter(|w| {
w.has_tag(&self.id)
&& w.transient.unwrap_or_else(|| 0.into()) == handle
&& w.is_managed()
})
.for_each(|w| {
w.set_visible(true);
});
} else {
// Don't bother updating the other windows when a window is fullscreen.
// Mark all windows for this workspace as visible.
let mut all_mine: Vec<&mut Window> =
windows.iter_mut().filter(|w| w.has_tag(&self.id)).collect();
all_mine.iter_mut().for_each(|w| w.set_visible(true));
// Update the location of all non-floating windows.
let mut managed_nonfloat: Vec<&mut Window> = windows
.iter_mut()
.filter(|w| w.has_tag(&self.id) && w.is_managed() && !w.floating())
.collect();
self.layout
.update_windows(workspace, &mut managed_nonfloat, self);
for w in &mut managed_nonfloat {
w.container_size = Some(workspace.xyhw);
}
// Update the location of all floating windows.
windows
.iter_mut()
.filter(|w| w.has_tag(&self.id) && w.is_managed() && w.floating())
.for_each(|w| w.normal = workspace.xyhw);
}
}
/// Changes the main width percentage by the provided delta.
/// Result is sanitized, so the percentage can't go below 0 or above 100.
///
/// ## Arguments
/// * `delta` - increase/decrease main width percentage by this amount
pub fn change_main_width(&mut self, delta: i8) {
// not smaller than 0 and not larger than 100
self.main_width_percentage = (self.main_width_percentage as i8 + delta).clamp(0, 100) as u8;
}
/// Sets the main width percentage
///
/// ## Arguments
/// * `val` - the new with percentage
pub fn set_main_width(&mut self, val: u8) {
self.main_width_percentage = val.min(100); // not larger than 100
}
#[must_use]
pub fn main_width_percentage(&self) -> f32 {
f32::from(self.main_width_percentage)
}
pub fn set_layout(&mut self, layout: Layout, main_width_percentage: u8) {
self.layout = layout;
self.set_main_width(main_width_percentage);
self.layout_rotation = 0;
}
pub fn rotate_layout(&mut self) -> Option<()> {
let rotations = self.layout.rotations();
self.layout_rotation += 1;
if self.layout_rotation >= rotations.len() {
self.layout_rotation = 0;
}
let (horz, vert) = rotations.get(self.layout_rotation)?;
self.flipped_horizontal = *horz;
self.flipped_vertical = *vert;
Some(())
}
}
#[cfg(test)]
mod tests {
use super::Tags;
use crate::layouts::Layout;
#[test]
fn normal_tags_are_numbered_in_order() {
let mut tags = Tags::new();
let home_id = tags.add_new("home", Layout::default());
let chat_id = tags.add_new("chat", Layout::default());
let surf_id = tags.add_new("surf", Layout::default());
let code_id = tags.add_new("code", Layout::default());
assert_eq!(home_id, 1);
assert_eq!(chat_id, 2);
assert_eq!(surf_id, 3);
assert_eq!(code_id, 4);
}
#[test]
fn hidden_tags_are_numbered_in_order() {
let mut tags = Tags::new();
let nsp_id = tags.add_new_hidden("NSP");
let whatver_id = tags.add_new_hidden("whatever");
assert_eq!(nsp_id, Some(usize::MAX));
assert_eq!(whatver_id, Some(usize::MAX - 1));
}
#[test]
fn multiple_normal_tags_can_have_same_label() {
let mut tags = Tags::new();
let first_id = tags.add_new("home", Layout::default());
let second_id = tags.add_new("home", Layout::default());
assert_eq!(first_id, 1);
assert_eq!(second_id, 2);
}
#[test]
fn unlabelled_tags_are_automatically_labelled_with_their_id() {
let mut tags = Tags::new();
let first_tag = tags.add_new_unlabeled(Layout::default());
let second_tag = tags.add_new_unlabeled(Layout::default());
let first_label = tags.get(first_tag).map(|tag| tag.label.clone());
let second_label = tags.get(second_tag).map(|tag| tag.label.clone());
assert_eq!(first_label, Some(String::from("1")));
assert_eq!(second_label, Some(String::from("2")));
}
#[test]
fn hidden_tags_must_have_unique_label() {
let mut tags = Tags::new();
let first_tag = tags.add_new_hidden("NSP");
let second_tag = tags.add_new_hidden("NSP");
let third_tag = tags.add_new_hidden("something-unique");
assert_eq!(first_tag, Some(usize::MAX));
assert_eq!(second_tag, None);
assert_eq!(third_tag, Some(usize::MAX - 1));
assert_eq!(tags.all().len(), 2); // the second tag must not be created
}
#[test]
fn must_be_able_to_only_get_normal_tags() {
let mut tags = Tags::new();
tags.add_new("home", Layout::default());
tags.add_new("chat", Layout::default());
tags.add_new("surf", Layout::default());
tags.add_new("code", Layout::default());
tags.add_new_hidden("NSP");
assert_eq!(tags.len_normal(), 4);
assert_eq!(tags.normal().len(), 4);
}
#[test]
fn must_be_able_to_get_all_tags() {
let mut tags = Tags::new();
tags.add_new("home", Layout::default());
tags.add_new("chat", Layout::default());
tags.add_new("surf", Layout::default());
tags.add_new("code", Layout::default());
tags.add_new_hidden("NSP");
assert_eq!(tags.all().len(), 5);
}
#[test]
fn hidden_tags_must_be_retrievable_by_label() {
let mut tags = Tags::new();
tags.add_new("home", Layout::default());
tags.add_new_hidden("NSP");
tags.add_new_hidden("whatever");
let nsp_tag = tags.get_hidden_by_label("NSP");
let whatever_tag = tags.get_hidden_by_label("whatever");
let inexistent_tag = tags.get_hidden_by_label("inexistent");
assert!(nsp_tag.is_some());
assert!(whatever_tag.is_some());
assert!(inexistent_tag.is_none());
}
#[test]
fn only_hidden_tags_can_be_retrieved_by_label() {
let mut tags = Tags::new();
tags.add_new("home", Layout::default());
let tag = tags.get_hidden_by_label("home");
assert!(tag.is_none());
}
#[test]
fn tags_can_be_mutable() {
let mut tags = Tags::new();
tags.add_new("home", Layout::default());
tags.add_new("chat", Layout::default());
tags.add_new("surf", Layout::default());
let first_retrieve = tags.get_mut(2).unwrap();
assert_eq!(first_retrieve.label, String::from("chat"));
first_retrieve.label = String::from("code");
let second_retrieve = tags.get_mut(2).unwrap();
assert_eq!(second_retrieve.label, String::from("code"));
}
}