1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
//! **The tabs between frames** (DESIGN §4.44 item 2; bl-03c1): which
//! conversations the operator has open, and the four things that change it.
//!
//! **Open is a navigation the operator performed** — [`Model::opened`]'s
//! class of fact, which no read can answer — so it is held, per run and never
//! in the place file. It is filled by [`Model::select`] and by nothing else:
//! every door that selects opens a tab by selecting, so there is no second act
//! that opens one. It is emptied by the close, and by the engine: a tab whose
//! conversation the aimed wall's listing no longer knows leaves with it, and a
//! selection the engine put down (bl-6dfd) is a tab put down.
use super::start::Phase;
use super::{Aim, Model};
/// **One open conversation**, under the wall it was selected on.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tab {
/// The wall it was opened under: the strip paints the aimed wall's.
pub aim: Aim,
/// The id every gesture addresses it with.
pub root_id: String,
}
impl Model {
/// **The aimed wall's open conversations**, by root id, in the order they
/// were first selected — what the strip paints.
pub fn strip(&self) -> Vec<String> {
self.tabs
.iter()
.filter(|tab| Some(&tab.aim) == self.aim.as_ref())
.map(|tab| tab.root_id.clone())
.collect()
}
/// **Open `root_id`'s tab** under the aim, unless it is open already.
pub(super) fn open_tab(&mut self, root_id: &str) {
let Some(aim) = self.aim.clone() else {
return;
};
let tab = Tab {
aim,
root_id: root_id.to_owned(),
};
if !self.tabs.contains(&tab) {
self.tabs.push(tab);
}
}
/// **Close one of the aimed wall's tabs.** Where it was the selected one,
/// the neighbour to its left is selected — to its right at the left end —
/// and where it was the last, the selection is put down to the wall.
pub fn close_tab(&mut self, root_id: &str) {
let strip = self.strip();
let Some(at) = strip.iter().position(|id| id == root_id) else {
return;
};
self.shut(root_id);
if self.conversation.as_deref() != Some(root_id) {
return;
}
let left = at.checked_sub(1).and_then(|left| strip.get(left));
if let Some(next) = left.or_else(|| strip.get(at + 1)) {
self.select(next);
} else {
self.moving_to(None);
self.deselect();
}
}
/// **Drop the aimed wall's tab on `root_id`**, whatever is selected.
pub(super) fn shut(&mut self, root_id: &str) {
let aim = self.aim.clone();
self.tabs
.retain(|tab| !(Some(&tab.aim) == aim.as_ref() && tab.root_id == root_id));
}
/// **A start's tab takes the root the engine minted** ([`Model::resolve`]):
/// it was opened under the claimed name, and it is the same conversation.
pub(super) fn rename_tab(&mut self, name: &str, root_id: &str) {
let renamed: Vec<Tab> = self
.tabs
.drain(..)
.map(|tab| Tab {
root_id: if tab.root_id == name {
root_id.to_owned()
} else {
tab.root_id
},
aim: tab.aim,
})
.collect();
for tab in renamed {
if !self.tabs.contains(&tab) {
self.tabs.push(tab);
}
}
}
/// **The listing answered: the tabs it no longer knows leave with it** —
/// only under the wall it answered ([`Model::answered`]), and never a
/// start's claimed name, which no listing knows until it resolves.
pub(super) fn prune_tabs(&mut self) {
let Some(answered) = self.answered.clone() else {
return;
};
let claimed = match self.start.as_ref().map(|start| &start.phase) {
Some(Phase::Started(name)) => Some(name.clone()),
_ => None,
};
let convs = &self.convs;
self.tabs.retain(|tab| {
tab.aim != answered
|| claimed.as_deref() == Some(tab.root_id.as_str())
|| convs.iter().any(|row| row.root_id == tab.root_id)
});
}
}
#[cfg(test)]
mod tests;