refactor: tabs rewrite to clean up API
Tabs relied heavily on `ViewExt`'s `title()` function, while also requiring a separate `id`. The `id`, if used, should be an internal part of the struct and not part of its API. This also removes the hashmap as it will never be faster than sequentially looking up all the names, since there will most likely never be that many tabs in a `TabbedView`.
This commit is contained in:
committed by
Henrik Friedrichsen
parent
b37fb7cc10
commit
0a1a9bdd4d
@@ -104,3 +104,79 @@ impl<V: ViewExt> IntoBoxedViewExt for V {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BoxedViewExt {
|
||||
boxed_view: Box<dyn ViewExt>,
|
||||
}
|
||||
|
||||
impl BoxedViewExt {
|
||||
pub fn new(view: Box<dyn ViewExt>) -> Self {
|
||||
Self { boxed_view: view }
|
||||
}
|
||||
}
|
||||
|
||||
impl View for BoxedViewExt {
|
||||
fn draw(&self, printer: &cursive::Printer) {
|
||||
self.boxed_view.draw(printer);
|
||||
}
|
||||
|
||||
fn layout(&mut self, xy: cursive::Vec2) {
|
||||
self.boxed_view.layout(xy);
|
||||
}
|
||||
|
||||
fn needs_relayout(&self) -> bool {
|
||||
self.boxed_view.needs_relayout()
|
||||
}
|
||||
|
||||
fn required_size(&mut self, constraint: cursive::Vec2) -> cursive::Vec2 {
|
||||
self.boxed_view.required_size(constraint)
|
||||
}
|
||||
|
||||
fn on_event(&mut self, event: cursive::event::Event) -> cursive::event::EventResult {
|
||||
self.boxed_view.on_event(event)
|
||||
}
|
||||
|
||||
fn call_on_any(&mut self, selector: &cursive::view::Selector, callback: cursive::event::AnyCb) {
|
||||
self.boxed_view.call_on_any(selector, callback);
|
||||
}
|
||||
|
||||
fn focus_view(
|
||||
&mut self,
|
||||
selector: &cursive::view::Selector,
|
||||
) -> Result<cursive::event::EventResult, cursive::view::ViewNotFound> {
|
||||
self.boxed_view.focus_view(selector)
|
||||
}
|
||||
|
||||
fn take_focus(
|
||||
&mut self,
|
||||
source: cursive::direction::Direction,
|
||||
) -> Result<cursive::event::EventResult, cursive::view::CannotFocus> {
|
||||
self.boxed_view.take_focus(source)
|
||||
}
|
||||
|
||||
fn important_area(&self, view_size: cursive::Vec2) -> cursive::Rect {
|
||||
self.boxed_view.important_area(view_size)
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
std::any::type_name::<Self>()
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewExt for BoxedViewExt {
|
||||
fn title(&self) -> String {
|
||||
self.boxed_view.title()
|
||||
}
|
||||
|
||||
fn title_sub(&self) -> String {
|
||||
self.boxed_view.title_sub()
|
||||
}
|
||||
|
||||
fn on_leave(&self) {
|
||||
self.boxed_view.on_leave();
|
||||
}
|
||||
|
||||
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||
self.boxed_view.on_command(s, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ use crate::model::artist::Artist;
|
||||
use crate::queue::Queue;
|
||||
use crate::traits::ViewExt;
|
||||
use crate::ui::listview::ListView;
|
||||
use crate::ui::tabview::TabView;
|
||||
use crate::ui::tabbedview::TabbedView;
|
||||
|
||||
pub struct AlbumView {
|
||||
album: Album,
|
||||
tabs: TabView,
|
||||
tabs: TabbedView,
|
||||
}
|
||||
|
||||
impl AlbumView {
|
||||
@@ -37,27 +37,26 @@ impl AlbumView {
|
||||
.map(|(id, name)| Artist::new(id.clone(), name.clone()))
|
||||
.collect();
|
||||
|
||||
let tabs = TabView::new()
|
||||
.tab(
|
||||
"tracks",
|
||||
ListView::new(
|
||||
Arc::new(RwLock::new(tracks)),
|
||||
queue.clone(),
|
||||
library.clone(),
|
||||
)
|
||||
.with_title("Tracks"),
|
||||
)
|
||||
.tab(
|
||||
"artists",
|
||||
ListView::new(Arc::new(RwLock::new(artists)), queue, library).with_title("Artists"),
|
||||
);
|
||||
let mut tabs = TabbedView::new();
|
||||
tabs.add_tab(
|
||||
"Tracks",
|
||||
ListView::new(
|
||||
Arc::new(RwLock::new(tracks)),
|
||||
queue.clone(),
|
||||
library.clone(),
|
||||
),
|
||||
);
|
||||
tabs.add_tab(
|
||||
"Artists",
|
||||
ListView::new(Arc::new(RwLock::new(artists)), queue, library),
|
||||
);
|
||||
|
||||
Self { album, tabs }
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewWrapper for AlbumView {
|
||||
wrap_impl!(self.tabs: TabView);
|
||||
wrap_impl!(self.tabs: TabbedView);
|
||||
}
|
||||
|
||||
impl ViewExt for AlbumView {
|
||||
|
||||
@@ -14,11 +14,11 @@ use crate::model::track::Track;
|
||||
use crate::queue::Queue;
|
||||
use crate::traits::ViewExt;
|
||||
use crate::ui::listview::ListView;
|
||||
use crate::ui::tabview::TabView;
|
||||
use crate::ui::tabbedview::TabbedView;
|
||||
|
||||
pub struct ArtistView {
|
||||
artist: Artist,
|
||||
tabs: TabView,
|
||||
tabs: TabbedView,
|
||||
}
|
||||
|
||||
impl ArtistView {
|
||||
@@ -61,34 +61,27 @@ impl ArtistView {
|
||||
});
|
||||
}
|
||||
|
||||
let mut tabs = TabView::new();
|
||||
let mut tabs = TabbedView::new();
|
||||
|
||||
if let Some(tracks) = artist.tracks.as_ref() {
|
||||
let tracks = tracks.clone();
|
||||
|
||||
tabs.add_tab(
|
||||
"tracks",
|
||||
"Saved Tracks",
|
||||
ListView::new(
|
||||
Arc::new(RwLock::new(tracks)),
|
||||
queue.clone(),
|
||||
library.clone(),
|
||||
)
|
||||
.with_title("Saved Tracks"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
tabs.add_tab(
|
||||
"top_tracks",
|
||||
ListView::new(top_tracks, queue.clone(), library.clone()).with_title("Top 10"),
|
||||
);
|
||||
|
||||
tabs.add_tab("albums", albums_view.with_title("Albums"));
|
||||
tabs.add_tab("singles", singles_view.with_title("Singles"));
|
||||
|
||||
tabs.add_tab(
|
||||
"related",
|
||||
ListView::new(related, queue, library).with_title("Related Artists"),
|
||||
"Top 10",
|
||||
ListView::new(top_tracks, queue.clone(), library.clone()),
|
||||
);
|
||||
tabs.add_tab("Albums", albums_view);
|
||||
tabs.add_tab("Singles", singles_view);
|
||||
tabs.add_tab("Related Artists", ListView::new(related, queue, library));
|
||||
|
||||
Self {
|
||||
artist: artist.clone(),
|
||||
@@ -116,7 +109,7 @@ impl ArtistView {
|
||||
}
|
||||
|
||||
impl ViewWrapper for ArtistView {
|
||||
wrap_impl!(self.tabs: TabView);
|
||||
wrap_impl!(self.tabs: TabbedView);
|
||||
}
|
||||
|
||||
impl ViewExt for ArtistView {
|
||||
|
||||
@@ -13,16 +13,16 @@ use crate::traits::ViewExt;
|
||||
use crate::ui::browse::BrowseView;
|
||||
use crate::ui::listview::ListView;
|
||||
use crate::ui::playlists::PlaylistsView;
|
||||
use crate::ui::tabview::TabView;
|
||||
use crate::ui::tabbedview::TabbedView;
|
||||
|
||||
pub struct LibraryView {
|
||||
tabs: TabView,
|
||||
tabs: TabbedView,
|
||||
display_name: Option<String>,
|
||||
}
|
||||
|
||||
impl LibraryView {
|
||||
pub fn new(queue: Arc<Queue>, library: Arc<Library>) -> Self {
|
||||
let mut tabview = TabView::new();
|
||||
let mut tabview = TabbedView::new();
|
||||
let selected_tabs = library
|
||||
.cfg
|
||||
.values()
|
||||
@@ -33,31 +33,27 @@ impl LibraryView {
|
||||
for tab in selected_tabs {
|
||||
match tab {
|
||||
LibraryTab::Tracks => tabview.add_tab(
|
||||
"tracks",
|
||||
ListView::new(library.tracks.clone(), queue.clone(), library.clone())
|
||||
.with_title("Tracks"),
|
||||
"Tracks",
|
||||
ListView::new(library.tracks.clone(), queue.clone(), library.clone()),
|
||||
),
|
||||
LibraryTab::Albums => tabview.add_tab(
|
||||
"albums",
|
||||
ListView::new(library.albums.clone(), queue.clone(), library.clone())
|
||||
.with_title("Albums"),
|
||||
"Albums",
|
||||
ListView::new(library.albums.clone(), queue.clone(), library.clone()),
|
||||
),
|
||||
LibraryTab::Artists => tabview.add_tab(
|
||||
"artists",
|
||||
ListView::new(library.artists.clone(), queue.clone(), library.clone())
|
||||
.with_title("Artists"),
|
||||
"Artists",
|
||||
ListView::new(library.artists.clone(), queue.clone(), library.clone()),
|
||||
),
|
||||
LibraryTab::Playlists => tabview.add_tab(
|
||||
"playlists",
|
||||
"Playlists",
|
||||
PlaylistsView::new(queue.clone(), library.clone()),
|
||||
),
|
||||
LibraryTab::Podcasts => tabview.add_tab(
|
||||
"podcasts",
|
||||
ListView::new(library.shows.clone(), queue.clone(), library.clone())
|
||||
.with_title("Podcasts"),
|
||||
"Podcasts",
|
||||
ListView::new(library.shows.clone(), queue.clone(), library.clone()),
|
||||
),
|
||||
LibraryTab::Browse => {
|
||||
tabview.add_tab("browse", BrowseView::new(queue.clone(), library.clone()))
|
||||
tabview.add_tab("Browse", BrowseView::new(queue.clone(), library.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +73,7 @@ impl LibraryView {
|
||||
}
|
||||
|
||||
impl ViewWrapper for LibraryView {
|
||||
wrap_impl!(self.tabs: TabView);
|
||||
wrap_impl!(self.tabs: TabbedView);
|
||||
}
|
||||
|
||||
impl ViewExt for LibraryView {
|
||||
|
||||
@@ -18,7 +18,7 @@ pub mod search;
|
||||
pub mod search_results;
|
||||
pub mod show;
|
||||
pub mod statusbar;
|
||||
pub mod tabview;
|
||||
pub mod tabbedview;
|
||||
|
||||
#[cfg(feature = "cover")]
|
||||
pub mod cover;
|
||||
|
||||
@@ -26,7 +26,7 @@ use crate::ui::layout::Layout;
|
||||
use crate::ui::listview::ListView;
|
||||
use crate::ui::pagination::Pagination;
|
||||
use crate::ui::search_results::SearchResultsView;
|
||||
use crate::ui::tabview::TabView;
|
||||
use crate::ui::tabbedview::TabbedView;
|
||||
use rspotify::model::search::SearchResult;
|
||||
|
||||
pub struct SearchView {
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::spotify_url::SpotifyUrl;
|
||||
use crate::traits::{ListItem, ViewExt};
|
||||
use crate::ui::listview::ListView;
|
||||
use crate::ui::pagination::Pagination;
|
||||
use crate::ui::tabview::TabView;
|
||||
use crate::ui::tabbedview::TabbedView;
|
||||
use cursive::view::ViewWrapper;
|
||||
use cursive::Cursive;
|
||||
use rspotify::model::search::SearchResult;
|
||||
@@ -35,7 +35,7 @@ pub struct SearchResultsView {
|
||||
pagination_shows: Pagination<Show>,
|
||||
results_episodes: Arc<RwLock<Vec<Episode>>>,
|
||||
pagination_episodes: Pagination<Episode>,
|
||||
tabs: TabView,
|
||||
tabs: TabbedView,
|
||||
spotify: Spotify,
|
||||
events: EventManager,
|
||||
}
|
||||
@@ -71,13 +71,13 @@ impl SearchResultsView {
|
||||
let list_episodes = ListView::new(results_episodes.clone(), queue.clone(), library);
|
||||
let pagination_episodes = list_episodes.get_pagination().clone();
|
||||
|
||||
let tabs = TabView::new()
|
||||
.tab("tracks", list_tracks.with_title("Tracks"))
|
||||
.tab("albums", list_albums.with_title("Albums"))
|
||||
.tab("artists", list_artists.with_title("Artists"))
|
||||
.tab("playlists", list_playlists.with_title("Playlists"))
|
||||
.tab("shows", list_shows.with_title("Podcasts"))
|
||||
.tab("episodes", list_episodes.with_title("Podcast Episodes"));
|
||||
let mut tabs = TabbedView::new();
|
||||
tabs.add_tab("Tracks", list_tracks);
|
||||
tabs.add_tab("Albums", list_albums);
|
||||
tabs.add_tab("Artists", list_artists);
|
||||
tabs.add_tab("Playlists", list_playlists);
|
||||
tabs.add_tab("Shows", list_shows);
|
||||
tabs.add_tab("Episodes", list_episodes);
|
||||
|
||||
let mut view = Self {
|
||||
search_term,
|
||||
@@ -403,7 +403,7 @@ impl SearchResultsView {
|
||||
&query,
|
||||
None,
|
||||
);
|
||||
self.tabs.move_focus_to(0);
|
||||
self.tabs.set_selected(0);
|
||||
}
|
||||
UriType::Album => {
|
||||
self.perform_search(
|
||||
@@ -412,7 +412,7 @@ impl SearchResultsView {
|
||||
&query,
|
||||
None,
|
||||
);
|
||||
self.tabs.move_focus_to(1);
|
||||
self.tabs.set_selected(1);
|
||||
}
|
||||
UriType::Artist => {
|
||||
self.perform_search(
|
||||
@@ -421,7 +421,7 @@ impl SearchResultsView {
|
||||
&query,
|
||||
None,
|
||||
);
|
||||
self.tabs.move_focus_to(2);
|
||||
self.tabs.set_selected(2);
|
||||
}
|
||||
UriType::Playlist => {
|
||||
self.perform_search(
|
||||
@@ -430,7 +430,7 @@ impl SearchResultsView {
|
||||
&query,
|
||||
None,
|
||||
);
|
||||
self.tabs.move_focus_to(3);
|
||||
self.tabs.set_selected(3);
|
||||
}
|
||||
UriType::Show => {
|
||||
self.perform_search(
|
||||
@@ -439,7 +439,7 @@ impl SearchResultsView {
|
||||
&query,
|
||||
None,
|
||||
);
|
||||
self.tabs.move_focus_to(4);
|
||||
self.tabs.set_selected(4);
|
||||
}
|
||||
UriType::Episode => {
|
||||
self.perform_search(
|
||||
@@ -448,7 +448,7 @@ impl SearchResultsView {
|
||||
&query,
|
||||
None,
|
||||
);
|
||||
self.tabs.move_focus_to(5);
|
||||
self.tabs.set_selected(5);
|
||||
}
|
||||
}
|
||||
// Is the query a spotify URL?
|
||||
@@ -462,7 +462,7 @@ impl SearchResultsView {
|
||||
&url.id,
|
||||
None,
|
||||
);
|
||||
self.tabs.move_focus_to(0);
|
||||
self.tabs.set_selected(0);
|
||||
}
|
||||
UriType::Album => {
|
||||
self.perform_search(
|
||||
@@ -471,7 +471,7 @@ impl SearchResultsView {
|
||||
&url.id,
|
||||
None,
|
||||
);
|
||||
self.tabs.move_focus_to(1);
|
||||
self.tabs.set_selected(1);
|
||||
}
|
||||
UriType::Artist => {
|
||||
self.perform_search(
|
||||
@@ -480,7 +480,7 @@ impl SearchResultsView {
|
||||
&url.id,
|
||||
None,
|
||||
);
|
||||
self.tabs.move_focus_to(2);
|
||||
self.tabs.set_selected(2);
|
||||
}
|
||||
UriType::Playlist => {
|
||||
self.perform_search(
|
||||
@@ -489,7 +489,7 @@ impl SearchResultsView {
|
||||
&url.id,
|
||||
None,
|
||||
);
|
||||
self.tabs.move_focus_to(3);
|
||||
self.tabs.set_selected(3);
|
||||
}
|
||||
UriType::Show => {
|
||||
self.perform_search(
|
||||
@@ -498,7 +498,7 @@ impl SearchResultsView {
|
||||
&url.id,
|
||||
None,
|
||||
);
|
||||
self.tabs.move_focus_to(4);
|
||||
self.tabs.set_selected(4);
|
||||
}
|
||||
UriType::Episode => {
|
||||
self.perform_search(
|
||||
@@ -507,7 +507,7 @@ impl SearchResultsView {
|
||||
&url.id,
|
||||
None,
|
||||
);
|
||||
self.tabs.move_focus_to(5);
|
||||
self.tabs.set_selected(5);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -552,7 +552,7 @@ impl SearchResultsView {
|
||||
}
|
||||
|
||||
impl ViewWrapper for SearchResultsView {
|
||||
wrap_impl!(self.tabs: TabView);
|
||||
wrap_impl!(self.tabs: TabbedView);
|
||||
}
|
||||
|
||||
impl ViewExt for SearchResultsView {
|
||||
|
||||
217
src/ui/tabbedview.rs
Normal file
217
src/ui/tabbedview.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
use std::cmp::min;
|
||||
|
||||
use cursive::{
|
||||
align::HAlign,
|
||||
event::{Event, EventResult, MouseButton, MouseEvent},
|
||||
theme::ColorStyle,
|
||||
view::Nameable,
|
||||
views::NamedView,
|
||||
Cursive, Printer, Vec2, View,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
command::{Command, MoveAmount, MoveMode},
|
||||
commands::CommandResult,
|
||||
traits::{BoxedViewExt, IntoBoxedViewExt, ViewExt},
|
||||
};
|
||||
|
||||
/// A view that displays other views in a tab layout.
|
||||
#[derive(Default)]
|
||||
pub struct TabbedView {
|
||||
/// The list of tabs
|
||||
tabs: Vec<NamedView<BoxedViewExt>>,
|
||||
/// The index of the currently visible tab from `tabs`
|
||||
selected: usize,
|
||||
/// The size given to the last call to `layout()`
|
||||
last_layout_size: Vec2,
|
||||
}
|
||||
|
||||
impl TabbedView {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Add `view` as a new tab to the end of this [TabsView].
|
||||
pub fn add_tab(&mut self, title: impl Into<String>, view: impl IntoBoxedViewExt) {
|
||||
let tab = BoxedViewExt::new(view.into_boxed_view_ext()).with_name(title);
|
||||
self.tabs.push(tab);
|
||||
}
|
||||
|
||||
/// Return a mutable reference to the tab at `index`, or None if there is no tab at `index`.
|
||||
pub fn tab_mut(&mut self, index: usize) -> Option<&mut NamedView<BoxedViewExt>> {
|
||||
self.tabs.get_mut(index)
|
||||
}
|
||||
|
||||
/// Return a mutable reference to the selected tab, or None if there is no selected tab
|
||||
/// currently.
|
||||
pub fn selected_tab_mut(&mut self) -> Option<&mut NamedView<BoxedViewExt>> {
|
||||
self.tab_mut(self.selected)
|
||||
}
|
||||
|
||||
/// Return the amount of tabs in this view.
|
||||
pub fn len(&self) -> usize {
|
||||
self.tabs.len()
|
||||
}
|
||||
|
||||
/// Check whether there are tabs in this [TabsView].
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Set the tab at `index` as currently visible.
|
||||
pub fn set_selected(&mut self, index: usize) {
|
||||
self.selected = min(self.len().saturating_sub(1), index);
|
||||
}
|
||||
|
||||
/// Move the focus by `amount`, clipping at the edges.
|
||||
pub fn move_selected(&mut self, amount: isize) {
|
||||
self.selected = min(
|
||||
self.selected.saturating_add_signed(amount),
|
||||
self.len().saturating_sub(1),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn move_left(&mut self) {
|
||||
self.move_selected(-1);
|
||||
}
|
||||
|
||||
pub fn move_right(&mut self) {
|
||||
self.move_selected(1);
|
||||
}
|
||||
|
||||
/// Move the focus to the first tab.
|
||||
pub fn select_first(&mut self) {
|
||||
self.selected = 0;
|
||||
}
|
||||
|
||||
/// Move the focus to the last tab.
|
||||
pub fn select_last(&mut self) {
|
||||
self.selected = self.len() - 1;
|
||||
}
|
||||
|
||||
/// Return whether we are on the first tab.
|
||||
pub fn on_first_tab(&mut self) -> bool {
|
||||
self.selected == 0
|
||||
}
|
||||
|
||||
/// Return whether we are on the last tab.
|
||||
pub fn on_last_tab(&mut self) -> bool {
|
||||
self.selected == self.len() - 1
|
||||
}
|
||||
|
||||
/// Return the width of a single tab.
|
||||
///
|
||||
/// Keep in mind that this is an average. It's only provided to make sure all functions use the
|
||||
/// same calculation for tab width to prevent off-by-one errors.
|
||||
pub fn tab_width(&self) -> usize {
|
||||
self.last_layout_size.x / self.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl View for TabbedView {
|
||||
fn draw(&self, printer: &Printer<'_, '_>) {
|
||||
if self.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let tabwidth = self.tab_width();
|
||||
for (i, tab) in self.tabs.iter().enumerate() {
|
||||
let style = if self.selected == i {
|
||||
ColorStyle::highlight()
|
||||
} else {
|
||||
ColorStyle::primary()
|
||||
};
|
||||
|
||||
let mut width = tabwidth;
|
||||
if i == self.tabs.len() - 1 {
|
||||
width += printer.size.x % self.tabs.len();
|
||||
}
|
||||
|
||||
let title = tab.name();
|
||||
let offset = HAlign::Center.get_offset(title.width(), width);
|
||||
|
||||
printer.with_color(style, |printer| {
|
||||
printer.print_hline((i * tabwidth, 0), width, " ");
|
||||
printer.print((i * tabwidth + offset, 0), title);
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(tab) = self.tabs.get(self.selected) {
|
||||
let printer = printer
|
||||
.offset((0, 1))
|
||||
.cropped((printer.size.x, printer.size.y - 1));
|
||||
|
||||
tab.draw(&printer);
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(&mut self, size: Vec2) {
|
||||
self.last_layout_size = size;
|
||||
if let Some(tab) = self.tab_mut(self.selected) {
|
||||
tab.layout((size.x, size.y - 1).into())
|
||||
}
|
||||
}
|
||||
|
||||
fn on_event(&mut self, event: Event) -> EventResult {
|
||||
if let Event::Mouse {
|
||||
offset,
|
||||
position,
|
||||
event,
|
||||
} = event
|
||||
{
|
||||
let position = position.checked_sub(offset);
|
||||
if let Some(0) = position.map(|p| p.y) {
|
||||
match event {
|
||||
MouseEvent::WheelUp => self.move_left(),
|
||||
MouseEvent::WheelDown => self.move_right(),
|
||||
MouseEvent::Press(MouseButton::Left) => {
|
||||
let tabwidth = self.tab_width();
|
||||
if let Some(selected_tab) = position.and_then(|p| p.x.checked_div(tabwidth))
|
||||
{
|
||||
self.set_selected(selected_tab);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
return EventResult::consumed();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(tab) = self.tab_mut(self.selected) {
|
||||
tab.on_event(event.relativized((0, 1)))
|
||||
} else {
|
||||
EventResult::Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewExt for TabbedView {
|
||||
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||
match cmd {
|
||||
Command::Move(mode, amount) if matches!(mode, MoveMode::Left | MoveMode::Right) => {
|
||||
if matches!(mode, MoveMode::Left) && !self.on_first_tab() {
|
||||
match amount {
|
||||
MoveAmount::Extreme => self.select_first(),
|
||||
MoveAmount::Integer(amount) => self.move_selected(-(*amount) as isize),
|
||||
_ => (),
|
||||
}
|
||||
} else if matches!(mode, MoveMode::Right) && !self.on_last_tab() {
|
||||
match amount {
|
||||
MoveAmount::Extreme => self.select_last(),
|
||||
MoveAmount::Integer(amount) => self.move_selected(*amount as isize),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Ok(CommandResult::Consumed(None))
|
||||
}
|
||||
_ => {
|
||||
if let Some(tab) = self.selected_tab_mut() {
|
||||
tab.on_command(s, cmd)
|
||||
} else {
|
||||
Ok(CommandResult::Ignored)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use cursive::align::HAlign;
|
||||
use cursive::event::{Event, EventResult, MouseButton, MouseEvent};
|
||||
use cursive::theme::ColorStyle;
|
||||
use cursive::traits::View;
|
||||
use cursive::{Cursive, Printer, Vec2};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::command::{Command, MoveAmount, MoveMode};
|
||||
use crate::commands::CommandResult;
|
||||
use crate::traits::{IntoBoxedViewExt, ViewExt};
|
||||
|
||||
pub struct Tab {
|
||||
view: Box<dyn ViewExt>,
|
||||
}
|
||||
|
||||
pub struct TabView {
|
||||
tabs: Vec<Tab>,
|
||||
ids: HashMap<String, usize>,
|
||||
selected: usize,
|
||||
size: Vec2,
|
||||
}
|
||||
|
||||
impl TabView {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tabs: Vec::new(),
|
||||
ids: HashMap::new(),
|
||||
selected: 0,
|
||||
size: Vec2::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_tab<S: Into<String>, V: IntoBoxedViewExt>(&mut self, id: S, view: V) {
|
||||
let tab = Tab {
|
||||
view: view.into_boxed_view_ext(),
|
||||
};
|
||||
self.tabs.push(tab);
|
||||
self.ids.insert(id.into(), self.tabs.len() - 1);
|
||||
}
|
||||
|
||||
pub fn tab<S: Into<String>, V: IntoBoxedViewExt>(mut self, id: S, view: V) -> Self {
|
||||
self.add_tab(id, view);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn move_focus_to(&mut self, target: usize) {
|
||||
let len = self.tabs.len().saturating_sub(1);
|
||||
self.selected = min(target, len);
|
||||
}
|
||||
|
||||
pub fn move_focus(&mut self, delta: i32) {
|
||||
let new = self.selected as i32 + delta;
|
||||
self.move_focus_to(max(new, 0) as usize);
|
||||
}
|
||||
}
|
||||
|
||||
impl View for TabView {
|
||||
fn draw(&self, printer: &Printer<'_, '_>) {
|
||||
if self.tabs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let tabwidth = printer.size.x / self.tabs.len();
|
||||
for (i, tab) in self.tabs.iter().enumerate() {
|
||||
let style = if self.selected == i {
|
||||
ColorStyle::highlight()
|
||||
} else {
|
||||
ColorStyle::primary()
|
||||
};
|
||||
|
||||
let mut width = tabwidth;
|
||||
if i == self.tabs.len() - 1 {
|
||||
width += printer.size.x % self.tabs.len();
|
||||
}
|
||||
|
||||
let title = tab.view.title();
|
||||
let offset = HAlign::Center.get_offset(title.width(), width);
|
||||
|
||||
printer.with_color(style, |printer| {
|
||||
printer.print_hline((i * tabwidth, 0), width, " ");
|
||||
printer.print((i * tabwidth + offset, 0), &title);
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(tab) = self.tabs.get(self.selected) {
|
||||
let printer = printer
|
||||
.offset((0, 1))
|
||||
.cropped((printer.size.x, printer.size.y - 1));
|
||||
|
||||
tab.view.draw(&printer);
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(&mut self, size: Vec2) {
|
||||
self.size = size;
|
||||
if let Some(tab) = self.tabs.get_mut(self.selected) {
|
||||
tab.view.layout(Vec2::new(size.x, size.y - 1));
|
||||
}
|
||||
}
|
||||
|
||||
fn on_event(&mut self, event: Event) -> EventResult {
|
||||
if let Event::Mouse {
|
||||
offset,
|
||||
position,
|
||||
event,
|
||||
} = event
|
||||
{
|
||||
let position = position.checked_sub(offset);
|
||||
if let Some(0) = position.map(|p| p.y) {
|
||||
match event {
|
||||
MouseEvent::WheelUp => self.move_focus(-1),
|
||||
MouseEvent::WheelDown => self.move_focus(1),
|
||||
MouseEvent::Press(MouseButton::Left) => {
|
||||
let tabwidth = self.size.x / self.tabs.len();
|
||||
if let Some(selected_tab) = position.and_then(|p| p.x.checked_div(tabwidth))
|
||||
{
|
||||
self.move_focus_to(selected_tab);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
return EventResult::consumed();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(tab) = self.tabs.get_mut(self.selected) {
|
||||
tab.view.on_event(event.relativized((0, 1)))
|
||||
} else {
|
||||
EventResult::Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewExt for TabView {
|
||||
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||
if let Command::Move(mode, amount) = cmd {
|
||||
let last_idx = self.tabs.len() - 1;
|
||||
|
||||
match mode {
|
||||
MoveMode::Left if self.selected > 0 => {
|
||||
match amount {
|
||||
MoveAmount::Extreme => self.move_focus_to(0),
|
||||
MoveAmount::Integer(amount) => self.move_focus(-(*amount)),
|
||||
_ => (),
|
||||
}
|
||||
return Ok(CommandResult::Consumed(None));
|
||||
}
|
||||
MoveMode::Right if self.selected < last_idx => {
|
||||
match amount {
|
||||
MoveAmount::Extreme => self.move_focus_to(last_idx),
|
||||
MoveAmount::Integer(amount) => self.move_focus(*amount),
|
||||
_ => (),
|
||||
}
|
||||
return Ok(CommandResult::Consumed(None));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(tab) = self.tabs.get_mut(self.selected) {
|
||||
tab.view.on_command(s, cmd)
|
||||
} else {
|
||||
Ok(CommandResult::Ignored)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user