feat: mark pinned category tabs
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
- [x] bulk category editing: when on a certain category view, add bulk category editing, such that all channels saved to that category are updated with the new categories set.
|
- [x] bulk category editing: when on a certain category view, add bulk category editing, such that all channels saved to that category are updated with the new categories set.
|
||||||
- [ ] ui improvement: display the pinned status on category tab titles for pinned categories
|
- [x] ui improvement: display the pinned status on category tab titles for pinned categories
|
||||||
- [x] start the terebi player as a new instance explicitly:
|
- [x] start the terebi player as a new instance explicitly:
|
||||||
```sh
|
```sh
|
||||||
terebi --new-instance
|
terebi --new-instance
|
||||||
|
|||||||
+41
-24
@@ -46,6 +46,8 @@
|
|||||||
//! Esc returns to NORMAL without clearing the filter; another Esc closes. Search input editing shortcuts apply.
|
//! Esc returns to NORMAL without clearing the filter; another Esc closes. Search input editing shortcuts apply.
|
||||||
//! Matching selections survive filter edits; no matches leaves Enter/p without an action.
|
//! Matching selections survive filter edits; no matches leaves Enter/p without an action.
|
||||||
//! Pinned categories stay visible; browsing another category replaces the single temporary tab.
|
//! Pinned categories stay visible; browsing another category replaces the single temporary tab.
|
||||||
|
//! Pinned category tab titles use `[p]`, matching the selector; Search, Feed, and temporary tabs do not.
|
||||||
|
//! The marker shares selection styling and counts toward whole-label wrapping without changing category names.
|
||||||
//! Browsing a pinned category focuses its existing tab and removes the temporary tab.
|
//! Browsing a pinned category focuses its existing tab and removes the temporary tab.
|
||||||
//! Top-level `pinned_categories = ["Favorites"]` persists pins; missing category names are ignored.
|
//! Top-level `pinned_categories = ["Favorites"]` persists pins; missing category names are ignored.
|
||||||
//! Tab/Shift-Tab cycle only visible tabs; hidden views retain cached rows and selection.
|
//! Tab/Shift-Tab cycle only visible tabs; hidden views retain cached rows and selection.
|
||||||
@@ -1213,7 +1215,8 @@ impl App {
|
|||||||
let mut selected_row = 0;
|
let mut selected_row = 0;
|
||||||
for (index, view) in self.visible_views() {
|
for (index, view) in self.visible_views() {
|
||||||
let mut title = Span::raw(view.name.as_str());
|
let mut title = Span::raw(view.name.as_str());
|
||||||
let title_width = title.width() + 2; // One space of padding on each side.
|
let pinned = index >= 2 && self.config.pinned_categories.contains(&view.name);
|
||||||
|
let title_width = title.width() + 2 + if pinned { 4 } else { 0 }; // Padding and pin marker.
|
||||||
if row_width != 0 && row_width + 1 + title_width > usize::from(area.width) {
|
if row_width != 0 && row_width + 1 + title_width > usize::from(area.width) {
|
||||||
tab_lines.push(Line::default());
|
tab_lines.push(Line::default());
|
||||||
row_width = 0;
|
row_width = 0;
|
||||||
@@ -1226,7 +1229,11 @@ impl App {
|
|||||||
if row_width != 0 {
|
if row_width != 0 {
|
||||||
row.spans.push("│".into());
|
row.spans.push("│".into());
|
||||||
}
|
}
|
||||||
row.spans.extend([" ".into(), title, " ".into()]);
|
row.spans.push(" ".into());
|
||||||
|
if pinned {
|
||||||
|
row.spans.push(Span::styled("[p] ", title.style));
|
||||||
|
}
|
||||||
|
row.spans.extend([title, " ".into()]);
|
||||||
row_width += usize::from(row_width != 0) + title_width;
|
row_width += usize::from(row_width != 0) + title_width;
|
||||||
}
|
}
|
||||||
// Leave space for search, a usable results pane, status, and help.
|
// Leave space for search, a usable results pane, status, and help.
|
||||||
@@ -2175,40 +2182,50 @@ mod tests {
|
|||||||
.collect();
|
.collect();
|
||||||
app.view_index = 4;
|
app.view_index = 4;
|
||||||
app.mode = Mode::Insert;
|
app.mode = Mode::Insert;
|
||||||
for (width, height, language, japanese, chinese, cursor_y) in [
|
for (width, height) in [(30, 20), (56, 20), (55, 20), (9, 20), (30, 12)] {
|
||||||
(30, 20, Some((1, 1)), (1, 2), (14, 2), 4),
|
|
||||||
(56, 20, Some((17, 0)), (38, 0), (51, 0), 2),
|
|
||||||
(55, 20, Some((17, 0)), (38, 0), (1, 1), 3),
|
|
||||||
(9, 20, None, (1, 3), (1, 4), 6),
|
|
||||||
(30, 12, Some((1, 0)), (1, 1), (14, 1), 3),
|
|
||||||
] {
|
|
||||||
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
|
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| app.draw(frame, Duration::ZERO))
|
.draw(|frame| app.draw(frame, Duration::ZERO))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let tab_height = terminal.get_cursor_position().unwrap().y - 1;
|
||||||
let buffer = terminal.backend().buffer();
|
let buffer = terminal.backend().buffer();
|
||||||
if let Some((x, y)) = language {
|
let locate = |symbol: &str| {
|
||||||
for (offset, character) in "Language immersion".chars().enumerate() {
|
(0..tab_height)
|
||||||
assert_eq!(
|
.flat_map(|y| (0..width).map(move |x| (x, y)))
|
||||||
buffer.cell((x + offset as u16, y)).unwrap().symbol(),
|
.find(|&position| buffer.cell(position).unwrap().symbol() == symbol)
|
||||||
character.to_string(),
|
};
|
||||||
"whole label at width {width}, height {height}"
|
let chinese = locate("中").expect("selected category remains visible");
|
||||||
);
|
assert_eq!(buffer.cell(chinese).unwrap().fg, Color::Cyan);
|
||||||
}
|
|
||||||
}
|
|
||||||
assert_eq!(buffer.cell(japanese).unwrap().symbol(), "日");
|
|
||||||
assert_ne!(buffer.cell(japanese).unwrap().fg, Color::Cyan);
|
|
||||||
assert_eq!(buffer.cell(chinese).unwrap().symbol(), "中");
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer.cell((chinese.0 + 2, chinese.1)).unwrap().symbol(),
|
buffer.cell((chinese.0 + 2, chinese.1)).unwrap().symbol(),
|
||||||
"文"
|
"文"
|
||||||
);
|
);
|
||||||
assert_eq!(buffer.cell(chinese).unwrap().fg, Color::Cyan);
|
if width >= 16 {
|
||||||
|
let japanese = locate("日").expect("neighboring category remains visible");
|
||||||
|
for (offset, character) in "日本語だけ".chars().enumerate() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
terminal.get_cursor_position().unwrap(),
|
buffer
|
||||||
(1, cursor_y).into()
|
.cell((japanese.0 + offset as u16 * 2, japanese.1))
|
||||||
|
.unwrap()
|
||||||
|
.symbol(),
|
||||||
|
character.to_string(),
|
||||||
|
"category label must not split across rows"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
assert_ne!(buffer.cell(japanese).unwrap().fg, Color::Cyan);
|
||||||
|
}
|
||||||
|
if width >= 24 {
|
||||||
|
assert!(
|
||||||
|
(0..tab_height).any(|y| {
|
||||||
|
(0..width)
|
||||||
|
.map(|x| buffer.cell((x, y)).unwrap().symbol())
|
||||||
|
.collect::<String>()
|
||||||
|
.contains("Language immersion")
|
||||||
|
}),
|
||||||
|
"whole category label must fit after wrapping"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user