Implement cover drawing as optional feature
This commit is contained in:
committed by
Henrik Friedrichsen
parent
dfb60ee4be
commit
df87ff9bdd
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -1473,6 +1473,15 @@ dependencies = [
|
|||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ioctl-rs"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "607b0d5e3c8affe6744655ccd713c5d3763c09407e191cea94705f541fd45151"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iovec"
|
name = "iovec"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -1921,6 +1930,7 @@ dependencies = [
|
|||||||
"fern",
|
"fern",
|
||||||
"futures 0.1.30",
|
"futures 0.1.30",
|
||||||
"futures 0.3.13",
|
"futures 0.3.13",
|
||||||
|
"ioctl-rs",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"librespot-core",
|
"librespot-core",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ strum = "0.20.0"
|
|||||||
strum_macros = "0.20.1"
|
strum_macros = "0.20.1"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
ioctl-rs = { version = "0.2", optional = true }
|
||||||
|
|
||||||
[dependencies.cursive]
|
[dependencies.cursive]
|
||||||
version = "0.16.3"
|
version = "0.16.3"
|
||||||
@@ -61,6 +62,7 @@ portaudio_backend = ["librespot-playback/portaudio-backend"]
|
|||||||
termion_backend = ["cursive/termion-backend"]
|
termion_backend = ["cursive/termion-backend"]
|
||||||
mpris = ["dbus", "dbus-tree"]
|
mpris = ["dbus", "dbus-tree"]
|
||||||
notify = ["notify-rust"]
|
notify = ["notify-rust"]
|
||||||
|
cover = ["ioctl-rs"]
|
||||||
default = ["share_clipboard", "pulseaudio_backend", "mpris", "notify", "cursive/pancurses-backend"]
|
default = ["share_clipboard", "pulseaudio_backend", "mpris", "notify", "cursive/pancurses-backend"]
|
||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -101,6 +101,7 @@ depending on your desktop environment settings. Have a look at the
|
|||||||
* `F2`: Search
|
* `F2`: Search
|
||||||
* `F3`: Library
|
* `F3`: Library
|
||||||
* `d` deletes the currently selected playlist
|
* `d` deletes the currently selected playlist
|
||||||
|
* `F8`: Album art (if compiled with the `cover` feature)
|
||||||
* Tracks and playlists can be played using `Return` and queued using `Space`
|
* Tracks and playlists can be played using `Return` and queued using `Space`
|
||||||
* `.` will play the selected item after the currently playing track
|
* `.` will play the selected item after the currently playing track
|
||||||
* `p` will move to the currently playing track in the queue
|
* `p` will move to the currently playing track in the queue
|
||||||
@@ -236,3 +237,13 @@ search_match = "light red"
|
|||||||
|
|
||||||
More examples can be found in pull request
|
More examples can be found in pull request
|
||||||
https://github.com/hrkfdn/ncspot/pull/40.
|
https://github.com/hrkfdn/ncspot/pull/40.
|
||||||
|
|
||||||
|
### Cover Drawing
|
||||||
|
|
||||||
|
When compiled with the `cover` feature, `ncspot` can draw the album art of the current track in a dedicated view (`:focus cover` or `F8` by default) using [Überzug](https://github.com/seebye/ueberzug). For more information on installation and terminal compatibility, consult that repository.
|
||||||
|
|
||||||
|
To allow scaling the album art up beyond its resolution (640x640 for Spotify covers), use the config key `cover_max_scale`. This is especially useful for HiDPI displays:
|
||||||
|
|
||||||
|
```
|
||||||
|
cover_max_scale = 2
|
||||||
|
```
|
||||||
|
|||||||
@@ -363,6 +363,8 @@ impl CommandManager {
|
|||||||
kb.insert("F1".into(), Command::Focus("queue".into()));
|
kb.insert("F1".into(), Command::Focus("queue".into()));
|
||||||
kb.insert("F2".into(), Command::Focus("search".into()));
|
kb.insert("F2".into(), Command::Focus("search".into()));
|
||||||
kb.insert("F3".into(), Command::Focus("library".into()));
|
kb.insert("F3".into(), Command::Focus("library".into()));
|
||||||
|
#[cfg(feature = "cover")]
|
||||||
|
kb.insert("F8".into(), Command::Focus("cover".into()));
|
||||||
kb.insert("?".into(), Command::Help);
|
kb.insert("?".into(), Command::Help);
|
||||||
kb.insert("Backspace".into(), Command::Back);
|
kb.insert("Backspace".into(), Command::Back);
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub struct ConfigValues {
|
|||||||
pub gapless: Option<bool>,
|
pub gapless: Option<bool>,
|
||||||
pub shuffle: Option<bool>,
|
pub shuffle: Option<bool>,
|
||||||
pub repeat: Option<queue::RepeatSetting>,
|
pub repeat: Option<queue::RepeatSetting>,
|
||||||
|
pub cover_max_scale: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||||
|
|||||||
@@ -257,6 +257,9 @@ fn main() {
|
|||||||
|
|
||||||
let queueview = ui::queue::QueueView::new(queue.clone(), library.clone());
|
let queueview = ui::queue::QueueView::new(queue.clone(), library.clone());
|
||||||
|
|
||||||
|
#[cfg(feature = "cover")]
|
||||||
|
let coverview = ui::cover::CoverView::new(queue.clone(), library.clone(), &cfg);
|
||||||
|
|
||||||
let status = ui::statusbar::StatusBar::new(
|
let status = ui::statusbar::StatusBar::new(
|
||||||
queue.clone(),
|
queue.clone(),
|
||||||
library,
|
library,
|
||||||
@@ -268,6 +271,9 @@ fn main() {
|
|||||||
.screen("library", libraryview.with_name("library"))
|
.screen("library", libraryview.with_name("library"))
|
||||||
.screen("queue", queueview);
|
.screen("queue", queueview);
|
||||||
|
|
||||||
|
#[cfg(feature = "cover")]
|
||||||
|
layout.add_screen("cover", coverview.with_name("cover"));
|
||||||
|
|
||||||
// initial screen is library
|
// initial screen is library
|
||||||
layout.set_screen("library");
|
layout.set_screen("library");
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ pub trait ViewExt: View {
|
|||||||
"".into()
|
"".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_leave(&self) {}
|
||||||
|
|
||||||
fn on_command(&mut self, _s: &mut Cursive, _cmd: &Command) -> Result<CommandResult, String> {
|
fn on_command(&mut self, _s: &mut Cursive, _cmd: &Command) -> Result<CommandResult, String> {
|
||||||
Ok(CommandResult::Ignored)
|
Ok(CommandResult::Ignored)
|
||||||
}
|
}
|
||||||
@@ -65,6 +67,10 @@ impl<V: ViewExt> ViewExt for NamedView<V> {
|
|||||||
self.with_view(|v| v.title()).unwrap_or_default()
|
self.with_view(|v| v.title()).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_leave(&self) {
|
||||||
|
self.with_view(|v| v.on_leave());
|
||||||
|
}
|
||||||
|
|
||||||
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||||
self.with_view_mut(move |v| v.on_command(s, cmd)).unwrap()
|
self.with_view_mut(move |v| v.on_command(s, cmd)).unwrap()
|
||||||
}
|
}
|
||||||
|
|||||||
302
src/ui/cover.rs
Normal file
302
src/ui/cover.rs
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Child, Stdio};
|
||||||
|
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
use cursive::theme::{ColorStyle, ColorType, PaletteColor};
|
||||||
|
use cursive::{Cursive, Printer, Vec2, View};
|
||||||
|
use ioctl_rs::{ioctl, TIOCGWINSZ};
|
||||||
|
|
||||||
|
use crate::command::{Command, GotoMode};
|
||||||
|
use crate::commands::CommandResult;
|
||||||
|
use crate::library::Library;
|
||||||
|
use crate::queue::Queue;
|
||||||
|
use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt};
|
||||||
|
use crate::ui::album::AlbumView;
|
||||||
|
use crate::ui::artist::ArtistView;
|
||||||
|
use crate::Config;
|
||||||
|
|
||||||
|
pub struct CoverView {
|
||||||
|
queue: Arc<Queue>,
|
||||||
|
library: Arc<Library>,
|
||||||
|
loading: Arc<RwLock<HashSet<String>>>,
|
||||||
|
last_size: RwLock<Vec2>,
|
||||||
|
drawn_url: RwLock<Option<String>>,
|
||||||
|
ueberzug: RwLock<Option<Child>>,
|
||||||
|
font_size: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CoverView {
|
||||||
|
pub fn new(queue: Arc<Queue>, library: Arc<Library>, config: &Config) -> Self {
|
||||||
|
// Determine size of window both in pixels and chars
|
||||||
|
let (rows, cols, mut xpixels, mut ypixels) = unsafe {
|
||||||
|
let query: (u16, u16, u16, u16) = (0, 0, 0, 0);
|
||||||
|
ioctl(1, TIOCGWINSZ, &query);
|
||||||
|
query
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Determined window dimensions: {}x{}, {}x{}",
|
||||||
|
xpixels, ypixels, cols, rows
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine font size, considering max scale to prevent tiny covers on HiDPI screens
|
||||||
|
let scale = config.values().cover_max_scale.unwrap_or(1.0);
|
||||||
|
xpixels = ((xpixels as f32) / scale) as u16;
|
||||||
|
ypixels = ((ypixels as f32) / scale) as u16;
|
||||||
|
|
||||||
|
let font_size = Vec2::new((xpixels / cols) as usize, (ypixels / rows) as usize);
|
||||||
|
|
||||||
|
debug!("Determined font size: {}x{}", font_size.x, font_size.y);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
queue,
|
||||||
|
library,
|
||||||
|
ueberzug: RwLock::new(None),
|
||||||
|
loading: Arc::new(RwLock::new(HashSet::new())),
|
||||||
|
last_size: RwLock::new(Vec2::new(0, 0)),
|
||||||
|
drawn_url: RwLock::new(None),
|
||||||
|
font_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_cover(&self, url: String, mut draw_offset: Vec2, draw_size: Vec2) {
|
||||||
|
if draw_size.x <= 1 || draw_size.y <= 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let needs_redraw = {
|
||||||
|
let last_size = self.last_size.read().unwrap();
|
||||||
|
let drawn_url = self.drawn_url.read().unwrap();
|
||||||
|
*last_size != draw_size || drawn_url.as_ref() != Some(&url)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !needs_redraw {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = match self.cache_path(url.clone()) {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut img_size = Vec2::new(640, 640);
|
||||||
|
|
||||||
|
let draw_size_pxls = draw_size * self.font_size;
|
||||||
|
let ratio = f32::min(
|
||||||
|
f32::min(
|
||||||
|
draw_size_pxls.x as f32 / img_size.x as f32,
|
||||||
|
draw_size_pxls.y as f32 / img_size.y as f32,
|
||||||
|
),
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
img_size = Vec2::new(
|
||||||
|
(ratio * img_size.x as f32) as usize,
|
||||||
|
(ratio * img_size.y as f32) as usize,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ueberzug takes an area given in chars and fits the image to
|
||||||
|
// that area (from the top left). Since we want to center the
|
||||||
|
// image at least horizontally, we need to fiddle around a bit.
|
||||||
|
let mut size = img_size / self.font_size;
|
||||||
|
|
||||||
|
// Make sure there is equal space in chars on either side
|
||||||
|
if size.x % 2 != draw_size.x % 2 {
|
||||||
|
size.x -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure x is the bottleneck so full width is used
|
||||||
|
size.y = std::cmp::min(draw_size.y, size.y + 1);
|
||||||
|
|
||||||
|
// Round up since the bottom might have empty space within
|
||||||
|
// the designated box
|
||||||
|
draw_offset.x += (draw_size.x - size.x) / 2;
|
||||||
|
draw_offset.y += (draw_size.y - size.y) - (draw_size.y - size.y) / 2;
|
||||||
|
|
||||||
|
let cmd = format!("{{\"action\":\"add\",\"scaler\":\"fit_contain\",\"identifier\":\"cover\",\"x\":{},\"y\":{},\"width\":{},\"height\":{},\"path\":\"{}\"}}\n",
|
||||||
|
draw_offset.x, draw_offset.y,
|
||||||
|
size.x, size.y,
|
||||||
|
path.to_str().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = self.run_ueberzug_cmd(&cmd) {
|
||||||
|
error!("Failed to run Ueberzug: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut last_size = self.last_size.write().unwrap();
|
||||||
|
*last_size = draw_size;
|
||||||
|
|
||||||
|
let mut drawn_url = self.drawn_url.write().unwrap();
|
||||||
|
*drawn_url = Some(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_cover(&self) {
|
||||||
|
let mut drawn_url = self.drawn_url.write().unwrap();
|
||||||
|
*drawn_url = None;
|
||||||
|
|
||||||
|
let cmd = "{\"action\": \"remove\", \"identifier\": \"cover\"}\n";
|
||||||
|
if let Err(e) = self.run_ueberzug_cmd(cmd) {
|
||||||
|
error!("Failed to run Ueberzug: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_ueberzug_cmd(&self, cmd: &str) -> Result<(), std::io::Error> {
|
||||||
|
let mut ueberzug = self.ueberzug.write().unwrap();
|
||||||
|
|
||||||
|
if ueberzug.is_none() {
|
||||||
|
*ueberzug = Some(
|
||||||
|
std::process::Command::new("ueberzug")
|
||||||
|
.args(&["layer", "--silent"])
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdin = (*ueberzug).as_mut().unwrap().stdin.as_mut().unwrap();
|
||||||
|
stdin.write_all(cmd.as_bytes())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cache_path(&self, url: String) -> Option<PathBuf> {
|
||||||
|
let mut path = crate::config::cache_path("covers");
|
||||||
|
path.push(url.split("/").last().unwrap());
|
||||||
|
|
||||||
|
let mut loading = self.loading.write().unwrap();
|
||||||
|
if loading.contains(&url) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.insert(url.clone());
|
||||||
|
|
||||||
|
let loading_thread = self.loading.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Err(e) = download(url.clone(), path.clone()) {
|
||||||
|
error!("Failed to download cover: {}", e);
|
||||||
|
}
|
||||||
|
let mut loading = loading_thread.write().unwrap();
|
||||||
|
loading.remove(&url.clone());
|
||||||
|
});
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for CoverView {
|
||||||
|
fn draw(&self, printer: &Printer<'_, '_>) {
|
||||||
|
// Completely blank out screen
|
||||||
|
let style = ColorStyle::new(
|
||||||
|
ColorType::Palette(PaletteColor::Background),
|
||||||
|
ColorType::Palette(PaletteColor::Background),
|
||||||
|
);
|
||||||
|
printer.with_color(style, |printer| {
|
||||||
|
for i in 0..printer.size.y {
|
||||||
|
printer.print_hline((0, i), printer.size.x, " ");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let cover_url = self.queue.get_current().map(|t| t.cover_url()).flatten();
|
||||||
|
|
||||||
|
if let Some(url) = cover_url {
|
||||||
|
self.draw_cover(url, printer.offset, printer.size);
|
||||||
|
} else {
|
||||||
|
self.clear_cover();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
|
||||||
|
Vec2::new(constraint.x, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewExt for CoverView {
|
||||||
|
fn title(&self) -> String {
|
||||||
|
"Cover".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_leave(&self) {
|
||||||
|
self.clear_cover();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_command(&mut self, _s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||||
|
match cmd {
|
||||||
|
Command::Save => {
|
||||||
|
if let Some(mut track) = self.queue.get_current() {
|
||||||
|
track.save(self.library.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Delete => {
|
||||||
|
if let Some(mut track) = self.queue.get_current() {
|
||||||
|
track.unsave(self.library.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Share(_mode) => {
|
||||||
|
let url = self
|
||||||
|
.queue
|
||||||
|
.get_current()
|
||||||
|
.and_then(|t| t.as_listitem().share_url());
|
||||||
|
|
||||||
|
if let Some(url) = url {
|
||||||
|
#[cfg(feature = "share_clipboard")]
|
||||||
|
crate::sharing::write_share(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(CommandResult::Consumed(None));
|
||||||
|
}
|
||||||
|
Command::Goto(mode) => {
|
||||||
|
if let Some(track) = self.queue.get_current() {
|
||||||
|
let queue = self.queue.clone();
|
||||||
|
let library = self.library.clone();
|
||||||
|
|
||||||
|
match mode {
|
||||||
|
GotoMode::Album => {
|
||||||
|
if let Some(album) = track.album(queue.clone()) {
|
||||||
|
let view =
|
||||||
|
AlbumView::new(queue, library, &album).as_boxed_view_ext();
|
||||||
|
return Ok(CommandResult::View(view));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GotoMode::Artist => {
|
||||||
|
if let Some(artists) = track.artists() {
|
||||||
|
return match artists.len() {
|
||||||
|
0 => Ok(CommandResult::Consumed(None)),
|
||||||
|
// Always choose the first artist even with more because
|
||||||
|
// the cover image really doesn't play nice with the menu
|
||||||
|
_ => {
|
||||||
|
let view = ArtistView::new(queue, library, &artists[0])
|
||||||
|
.as_boxed_view_ext();
|
||||||
|
Ok(CommandResult::View(view))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(CommandResult::Ignored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download(url: String, path: PathBuf) -> Result<(), std::io::Error> {
|
||||||
|
let mut resp =
|
||||||
|
reqwest::get(&url).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||||
|
|
||||||
|
std::fs::create_dir_all(path.parent().unwrap())?;
|
||||||
|
let mut file = File::create(path)?;
|
||||||
|
|
||||||
|
std::io::copy(&mut resp, &mut file)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -70,6 +70,10 @@ impl Layout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_screen<S: Into<String>, T: IntoBoxedViewExt>(&mut self, id: S, view: T) {
|
pub fn add_screen<S: Into<String>, T: IntoBoxedViewExt>(&mut self, id: S, view: T) {
|
||||||
|
if let Some(view) = self.get_top_view() {
|
||||||
|
view.on_leave();
|
||||||
|
}
|
||||||
|
|
||||||
let s = id.into();
|
let s = id.into();
|
||||||
self.screens.insert(s.clone(), view.as_boxed_view_ext());
|
self.screens.insert(s.clone(), view.as_boxed_view_ext());
|
||||||
self.stack.insert(s.clone(), Vec::new());
|
self.stack.insert(s.clone(), Vec::new());
|
||||||
@@ -82,6 +86,10 @@ impl Layout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_screen<S: Into<String>>(&mut self, id: S) {
|
pub fn set_screen<S: Into<String>>(&mut self, id: S) {
|
||||||
|
if let Some(view) = self.get_top_view() {
|
||||||
|
view.on_leave();
|
||||||
|
}
|
||||||
|
|
||||||
let s = id.into();
|
let s = id.into();
|
||||||
self.focus = Some(s);
|
self.focus = Some(s);
|
||||||
self.cmdline_focus = false;
|
self.cmdline_focus = false;
|
||||||
@@ -113,12 +121,20 @@ impl Layout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_view(&mut self, view: Box<dyn ViewExt>) {
|
pub fn push_view(&mut self, view: Box<dyn ViewExt>) {
|
||||||
|
if let Some(view) = self.get_top_view() {
|
||||||
|
view.on_leave();
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(stack) = self.get_focussed_stack_mut() {
|
if let Some(stack) = self.get_focussed_stack_mut() {
|
||||||
stack.push(view)
|
stack.push(view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pop_view(&mut self) {
|
pub fn pop_view(&mut self) {
|
||||||
|
if let Some(view) = self.get_top_view() {
|
||||||
|
view.on_leave();
|
||||||
|
}
|
||||||
|
|
||||||
self.get_focussed_stack_mut().map(|stack| stack.pop());
|
self.get_focussed_stack_mut().map(|stack| stack.pop());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,6 @@ pub mod search_results;
|
|||||||
pub mod show;
|
pub mod show;
|
||||||
pub mod statusbar;
|
pub mod statusbar;
|
||||||
pub mod tabview;
|
pub mod tabview;
|
||||||
|
|
||||||
|
#[cfg(feature = "cover")]
|
||||||
|
pub mod cover;
|
||||||
|
|||||||
Reference in New Issue
Block a user