use super::*;
use crate::model::layouts as store;
use artifact::layout::{Kind as MemberKind, Layout, Member, Side};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Showing {
Session(u64),
Board(usize),
Article(usize),
Table(usize),
}
impl Workspace {
pub fn active_layout(&self) -> Option<&Layout> {
self.layouts.get(self.layout?)
}
pub fn active_layout_mut(&mut self) -> Option<&mut Layout> {
self.layouts.get_mut(self.layout?)
}
pub fn reload_layouts(&mut self, cx: &mut Context<Self>) {
let open = self.active_layout().map(|layout| layout.id.clone());
self.layouts = store::all();
self.layout = open.and_then(|id| self.layouts.iter().position(|at| at.id == id));
cx.notify();
}
pub fn open_layout(&mut self, ix: usize, cx: &mut Context<Self>) {
let Some(layout) = self.layouts.get(ix) else {
return;
};
let wanted: Vec<PathBuf> = layout
.entries()
.into_iter()
.map(|member| member.project)
.filter(|path| !self.projects.iter().any(|open| open.path == *path))
.filter(|path| path.is_dir())
.collect();
for path in wanted {
self.open_project(path, cx);
}
self.layout = Some(ix);
self.save();
cx.notify();
}
pub fn leave_layout(&mut self) {
self.layout = None;
}
pub fn arrange(
&mut self,
target: &Member,
arriving: &Member,
side: Side,
cx: &mut Context<Self>,
) -> Option<usize> {
let at = match self.layout {
Some(at) if at < self.layouts.len() => at,
_ => {
let layout = store::create("", target.clone())?;
self.layouts.insert(0, layout);
0
}
};
for (ix, other) in self.layouts.iter_mut().enumerate() {
if ix != at && other.remove(arriving) {
store::save(other);
}
}
let layout = self.layouts.get_mut(at)?;
if !layout.insert(target, arriving, side) {
return None;
}
store::save(layout);
self.layout = Some(at);
self.save();
cx.notify();
Some(at)
}
pub fn stack_pane(&mut self, target: &Member, arriving: &Member, cx: &mut Context<Self>) {
let Some(at) = self.layout else {
return;
};
for (ix, other) in self.layouts.iter_mut().enumerate() {
if ix != at && other.remove(arriving) {
store::save(other);
}
}
self.edit_layout(cx, |layout| layout.stack(target, arriving));
}
pub fn stack_of(&self, entry: &Member) -> Vec<Member> {
self.active_layout()
.map(|layout| layout.stack_of(entry))
.unwrap_or_default()
}
pub fn neighbour_pane(&self, entry: &Member, side: Side) -> Option<Member> {
self.active_layout()?.tree.neighbour(entry, side)
}
pub fn move_pane(&mut self, entry: &Member, side: Side, cx: &mut Context<Self>) -> bool {
let Some(across) = self.neighbour_pane(entry, side) else {
return false;
};
let mut moved = false;
self.edit_layout(cx, |layout| {
moved = layout.tree.swap(entry, &across);
moved
});
moved
}
pub fn relocate_pane(
&mut self,
entry: &Member,
target: &Member,
side: Side,
cx: &mut Context<Self>,
) {
self.edit_layout(cx, |layout| layout.relocate(entry, target, side));
}
pub fn close_pane(&mut self, entry: &Member, cx: &mut Context<Self>) {
let Some(layout) = self.active_layout() else {
return;
};
if layout.stack_of(entry).len() > 1 {
self.edit_layout(cx, |layout| layout.remove(entry));
return;
}
if layout.leaves() > 2 {
self.edit_layout(cx, |layout| layout.remove(entry));
return;
}
let survivor = layout
.entries()
.into_iter()
.find(|member| member != entry)
.and_then(|member| self.showing_of(&member));
if let Some(at) = self.layout {
self.delete_layout(at, cx);
}
if let Some((project, showing)) = survivor {
self.select_showing(project, showing, cx);
}
}
pub fn drop_from_layouts(&mut self, member: &Member, cx: &mut Context<Self>) {
let Some(ix) = self.layout_holding(member) else {
return;
};
let Some(layout) = self.layouts.get_mut(ix) else {
return;
};
if layout.leaves() <= 2 && layout.stack_of(member).len() <= 1 {
self.delete_layout(ix, cx);
return;
}
if layout.remove(member) {
store::save(layout);
cx.notify();
}
}
pub fn zoom_pane(&mut self, entry: &Member, cx: &mut Context<Self>) {
self.edit_layout(cx, |layout| {
layout.zoom(entry);
true
});
}
pub fn delete_layout(&mut self, ix: usize, cx: &mut Context<Self>) {
if ix >= self.layouts.len() {
return;
}
store::remove(&self.layouts.remove(ix).id);
self.layout = self
.layout
.filter(|open| *open != ix)
.map(|open| if open > ix { open - 1 } else { open });
self.save();
cx.notify();
}
pub fn archive_layout(&mut self, ix: usize, archived: bool, cx: &mut Context<Self>) {
let Some(layout) = self.layouts.get_mut(ix) else {
return;
};
layout.archived = archived;
store::save(layout);
if archived && self.layout == Some(ix) {
self.layout = None;
}
self.save();
cx.notify();
}
pub fn rename_layout(&mut self, id: &str, name: String, cx: &mut Context<Self>) {
if let Some(layout) = self.layouts.iter_mut().find(|layout| layout.id == id) {
layout.name = name.trim().to_owned();
store::save(layout);
cx.notify();
}
}
pub fn layout_holding(&self, member: &Member) -> Option<usize> {
self.layouts
.iter()
.position(|layout| !layout.archived && layout.contains(member))
}
pub fn prune_layouts(&mut self, cx: &mut Context<Self>) {
let mut live: Vec<Member> = Vec::new();
for open in &self.projects {
let path = open.path.clone();
for chat in &open.sessions {
if let Some(record) = chat.record.clone() {
live.push(Member::new(path.clone(), MemberKind::Session, record));
}
}
for board in &open.boards {
live.push(Member::new(
path.clone(),
MemberKind::Board,
board.id.clone(),
));
}
for article in &open.articles {
live.push(Member::new(
path.clone(),
MemberKind::Article,
article.path.to_string_lossy().into_owned(),
));
}
for table in &open.tables {
live.push(Member::new(
path.clone(),
MemberKind::Table,
table.key.clone(),
));
}
}
let open: Vec<PathBuf> = self.projects.iter().map(|open| open.path.clone()).collect();
for layout in &mut self.layouts {
let gone: Vec<Member> = layout
.entries()
.into_iter()
.filter(|member| open.contains(&member.project) && !live.contains(member))
.collect();
if !gone.is_empty() && layout.prune(&gone) {
store::save(layout);
}
}
cx.notify();
}
pub fn showing_of(&self, member: &Member) -> Option<(usize, Showing)> {
let at = self
.projects
.iter()
.position(|open| open.path == member.project)?;
let open = self.projects.get(at)?;
let showing = match member.kind {
MemberKind::Session => open
.sessions
.iter()
.find(|chat| chat.record.as_deref() == Some(member.id.as_str()))
.map(|chat| Showing::Session(chat.id))?,
MemberKind::Board => open
.boards
.iter()
.position(|board| board.id == member.id)
.map(Showing::Board)?,
MemberKind::Article => open
.articles
.iter()
.position(|article| article.path.to_string_lossy() == member.id)
.map(Showing::Article)?,
MemberKind::Table => open
.tables
.iter()
.position(|table| table.key == member.id)
.map(Showing::Table)?,
};
Some((at, showing))
}
pub fn member_of(&self, project: usize, showing: Showing) -> Option<Member> {
let open = self.projects.get(project)?;
let path = open.path.clone();
Some(match showing {
Showing::Session(id) => {
Member::new(path, MemberKind::Session, open.session(id)?.record.clone()?)
}
Showing::Board(ix) => {
Member::new(path, MemberKind::Board, open.boards.get(ix)?.id.clone())
}
Showing::Article(ix) => Member::new(
path,
MemberKind::Article,
open.articles.get(ix)?.path.to_string_lossy().into_owned(),
),
Showing::Table(ix) => {
Member::new(path, MemberKind::Table, open.tables.get(ix)?.key.clone())
}
})
}
pub fn board_in(&self, project: usize, ix: usize) -> Option<&Board> {
if !self.settings.features.boards {
return None;
}
self.projects.get(project)?.boards.get(ix)
}
pub fn article_in(&self, project: usize, ix: usize) -> Option<&Article> {
self.projects.get(project)?.articles.get(ix)
}
pub fn table_in(&self, project: usize, ix: usize) -> Option<&Table> {
if !self.settings.features.tables {
return None;
}
self.projects.get(project)?.tables.get(ix)
}
pub fn page_in(&self, project: usize, ix: usize) -> Option<&Page> {
if !self.settings.features.tables {
return None;
}
let open = self.projects.get(project)?;
open.pages.get(&open.tables.get(ix)?.key)
}
pub fn session_in(&self, project: usize, id: u64) -> Option<&ChatSession> {
self.projects.get(project)?.session(id)
}
pub fn select_showing(&mut self, project: usize, showing: Showing, cx: &mut Context<Self>) {
let text_size = self.article_font_size();
let Some(open) = self.projects.get_mut(project) else {
return;
};
match showing {
Showing::Session(id) => open.active = Some(id),
Showing::Board(ix) => {
if let Some(id) = open.boards.get(ix).map(|board| board.id.clone())
&& open.load_board(&id)
{
open.board = Some(ix);
}
}
Showing::Article(ix) => {
if let Some(article) = open.articles.get_mut(ix) {
article.open(text_size, cx);
open.article = Some(ix);
}
}
Showing::Table(ix) => {
open.table = Some(ix);
open.reload_page();
}
}
self.active = Some(project);
if let Showing::Session(id) = showing {
self.wake_session(id, cx);
}
cx.notify();
}
fn edit_layout(&mut self, cx: &mut Context<Self>, edit: impl FnOnce(&mut Layout) -> bool) {
let Some(layout) = self.layout.and_then(|ix| self.layouts.get_mut(ix)) else {
return;
};
if edit(layout) {
store::save(layout);
cx.notify();
}
}
}
#[cfg(test)]
#[path = "../../../tests/unit/layouts.rs"]
mod layout_tests;