diff --git a/src/app.rs b/src/app.rs index cc3ab7d..c944749 100644 --- a/src/app.rs +++ b/src/app.rs @@ -22,7 +22,6 @@ use crate::{ batch::BatchWidget, category::CategoryPopup, clients::ClientsPopup, - error::ErrorPopup, filter::FilterPopup, help::HelpPopup, notifications::NotificationWidget, @@ -86,7 +85,6 @@ pub enum Mode { Theme, Sources, Clients, - Error, Page, User, Help, @@ -105,7 +103,6 @@ impl Display for Mode { Mode::Sources => "Sources".to_owned(), Mode::Clients => "Clients".to_owned(), Mode::Loading(_) => "Loading".to_owned(), - Mode::Error => "Error".to_owned(), Mode::Page => "Page".to_owned(), Mode::User => "User".to_owned(), Mode::Help => "Help".to_owned(), @@ -139,7 +136,6 @@ pub struct Context { notifications: Vec, failed_config_load: bool, should_quit: bool, - should_redraw: bool, should_dismiss_notifications: bool, } @@ -156,10 +152,6 @@ impl Context { self.should_dismiss_notifications = true; } - pub fn redraw(&mut self) { - self.should_redraw = true; - } - pub fn save_config(&mut self) -> Result<(), Box> { #[cfg(not(feature = "integration-test"))] if !self.failed_config_load { @@ -194,7 +186,6 @@ impl Default for Context { deltatime: 0.0, failed_config_load: true, should_quit: false, - should_redraw: false, should_dismiss_notifications: false, } } @@ -212,7 +203,6 @@ pub struct Widgets { pub clients: ClientsPopup, pub search: SearchWidget, pub results: ResultsWidget, - pub error: ErrorPopup, pub page: PagePopup, pub user: UserPopup, pub help: HelpPopup, @@ -261,13 +251,17 @@ impl App { .for_each(|n| self.widgets.notification.add_notification(n)); ctx.notifications.clear(); } + if !ctx.errors.is_empty() { + ctx.errors + .clone() + .into_iter() + .for_each(|n| self.widgets.notification.add_error(n)); + ctx.errors.clear(); + } if ctx.should_dismiss_notifications { self.widgets.notification.dismiss_all(); ctx.should_dismiss_notifications = false; } - if !ctx.errors.is_empty() { - ctx.mode = Mode::Error; - } if ctx.mode == Mode::Batch && ctx.batch.is_empty() { ctx.mode = Mode::Normal; } @@ -409,11 +403,6 @@ impl App { } }; } - // if self.widgets.notification.is_animating() { - // let now = Instant::now(); - // ctx.deltatime = (now - last_time.unwrap_or(now)).as_secs_f64(); - // last_time = Some(now); - // } } Ok(()) } @@ -447,13 +436,6 @@ impl App { Mode::Sort(_) => self.widgets.sort.draw(f, ctx, f.size()), Mode::Filter => self.widgets.filter.draw(f, ctx, f.size()), Mode::Theme => self.widgets.theme.draw(f, ctx, f.size()), - Mode::Error => { - // Get the oldest error first - if let Some(error) = ctx.errors.pop_front() { - self.widgets.error.with_error(error); - } - self.widgets.error.draw(f, ctx, f.size()); - } Mode::Help => self.widgets.help.draw(f, ctx, f.size()), Mode::Page => self.widgets.page.draw(f, ctx, f.size()), Mode::User => self.widgets.user.draw(f, ctx, f.size()), @@ -508,7 +490,6 @@ impl App { Mode::Search => self.widgets.search.handle_event(ctx, evt), Mode::Filter => self.widgets.filter.handle_event(ctx, evt), Mode::Theme => self.widgets.theme.handle_event(ctx, evt), - Mode::Error => self.widgets.error.handle_event(ctx, evt), Mode::Page => self.widgets.page.handle_event(ctx, evt), Mode::User => self.widgets.user.handle_event(ctx, evt), Mode::Help => self.widgets.help.handle_event(ctx, evt), @@ -554,7 +535,6 @@ impl App { Mode::User => UserPopup::get_help(), Mode::Sources => SourcesPopup::get_help(), Mode::Clients => ClientsPopup::get_help(), - Mode::Error => None, Mode::Help => None, Mode::KeyCombo(_) => None, Mode::Loading(_) => None, diff --git a/src/client/qbit.rs b/src/client/qbit.rs index 7856ea1..a2c86fb 100644 --- a/src/client/qbit.rs +++ b/src/client/qbit.rs @@ -185,6 +185,7 @@ impl DownloadClient for QbitClient { conf: ClientConfig, client: reqwest::Client, ) -> DownloadResult { + // return DownloadResult::error(DownloadError("Failed to login :\\")); let Some(qbit) = conf.qbit.to_owned() else { return DownloadResult::error(DownloadError( "Failed to get qBittorrent config".to_owned(), diff --git a/src/util/conv.rs b/src/util/conv.rs index 61662fe..16bb1a4 100644 --- a/src/util/conv.rs +++ b/src/util/conv.rs @@ -69,36 +69,38 @@ pub fn key_to_string(key: KeyCode, modifier: KeyModifiers) -> String { KeyCode::Menu => "Menu".to_owned(), KeyCode::KeypadBegin => "Begin".to_owned(), KeyCode::Media(m) => match m { - MediaKeyCode::Play => "MediaPlay".to_owned(), - MediaKeyCode::Pause => "MediaPause".to_owned(), - MediaKeyCode::PlayPause => "MediaPlayPause".to_owned(), - MediaKeyCode::Reverse => "MediaReverse".to_owned(), - MediaKeyCode::Stop => "MediaStop".to_owned(), - MediaKeyCode::FastForward => "MediaFastForward".to_owned(), - MediaKeyCode::Rewind => "MediaRewind".to_owned(), - MediaKeyCode::TrackNext => "MediaTrackNext".to_owned(), - MediaKeyCode::TrackPrevious => "MediaTrackPrevious".to_owned(), - MediaKeyCode::Record => "MediaRecord".to_owned(), - MediaKeyCode::LowerVolume => "MediaLowerVolume".to_owned(), - MediaKeyCode::RaiseVolume => "MediaRaiseVolume".to_owned(), - MediaKeyCode::MuteVolume => "MediaMuteVolume".to_owned(), - }, + MediaKeyCode::Play => "MediaPlay", + MediaKeyCode::Pause => "MediaPause", + MediaKeyCode::PlayPause => "MediaPlayPause", + MediaKeyCode::Reverse => "MediaReverse", + MediaKeyCode::Stop => "MediaStop", + MediaKeyCode::FastForward => "MediaFastForward", + MediaKeyCode::Rewind => "MediaRewind", + MediaKeyCode::TrackNext => "MediaTrackNext", + MediaKeyCode::TrackPrevious => "MediaTrackPrevious", + MediaKeyCode::Record => "MediaRecord", + MediaKeyCode::LowerVolume => "MediaLowerVolume", + MediaKeyCode::RaiseVolume => "MediaRaiseVolume", + MediaKeyCode::MuteVolume => "MediaMuteVolume", + } + .to_owned(), KeyCode::Modifier(m) => match m { - ModifierKeyCode::LeftShift => "LeftShift".to_owned(), - ModifierKeyCode::LeftControl => "LeftControl".to_owned(), - ModifierKeyCode::LeftAlt => "LeftAlt".to_owned(), - ModifierKeyCode::LeftSuper => "LeftSuper".to_owned(), - ModifierKeyCode::LeftHyper => "LeftHyper".to_owned(), - ModifierKeyCode::LeftMeta => "LeftMeta".to_owned(), - ModifierKeyCode::RightShift => "RightShift".to_owned(), - ModifierKeyCode::RightControl => "RightControl".to_owned(), - ModifierKeyCode::RightAlt => "RightAlt".to_owned(), - ModifierKeyCode::RightSuper => "RightSuper".to_owned(), - ModifierKeyCode::RightHyper => "RightHyper".to_owned(), - ModifierKeyCode::RightMeta => "RightMeta".to_owned(), - ModifierKeyCode::IsoLevel3Shift => "IsoLevel3Shift".to_owned(), - ModifierKeyCode::IsoLevel5Shift => "IsoLevel5Shift".to_owned(), - }, + ModifierKeyCode::LeftShift => "LeftShift", + ModifierKeyCode::LeftControl => "LeftControl", + ModifierKeyCode::LeftAlt => "LeftAlt", + ModifierKeyCode::LeftSuper => "LeftSuper", + ModifierKeyCode::LeftHyper => "LeftHyper", + ModifierKeyCode::LeftMeta => "LeftMeta", + ModifierKeyCode::RightShift => "RightShift", + ModifierKeyCode::RightControl => "RightControl", + ModifierKeyCode::RightAlt => "RightAlt", + ModifierKeyCode::RightSuper => "RightSuper", + ModifierKeyCode::RightHyper => "RightHyper", + ModifierKeyCode::RightMeta => "RightMeta", + ModifierKeyCode::IsoLevel3Shift => "IsoLevel3Shift", + ModifierKeyCode::IsoLevel5Shift => "IsoLevel5Shift", + } + .to_owned(), }; let modifier = match modifier { KeyModifiers::CONTROL => "C-", diff --git a/src/widget.rs b/src/widget.rs index 11e1ed2..375c946 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -12,13 +12,13 @@ use ratatui::{ }, Frame, }; +use unicode_width::UnicodeWidthStr as _; use crate::{app::Context, style, theme::Theme}; pub mod batch; pub mod category; pub mod clients; -pub mod error; pub mod filter; pub mod help; pub mod input; @@ -125,6 +125,15 @@ pub fn scrollbar(ctx: &Context, orientation: ScrollbarOrientation) -> Scrollbar< } pub fn clear(area: Rect, buf: &mut Buffer, fill: Color) { + // Deal with wide chars which might extend too far + if area.left() > 0 && buf.area.contains((area.left() - 1, area.top()).into()) { + for i in area.top()..area.bottom() { + let c = buf.get_mut(area.left() - 1, i); + if c.symbol().width() > 1 { + c.set_char(' '); + } + } + } Clear.render(area, buf); Block::new().bg(fill).render(area, buf); } diff --git a/src/widget/category.rs b/src/widget/category.rs index 76f5214..dc70889 100644 --- a/src/widget/category.rs +++ b/src/widget/category.rs @@ -112,28 +112,17 @@ impl Widget for CategoryPopup { " ".into(), e.name.to_owned().into(), ])]) - // match i == self.minor { - // true => row.bg(ctx.theme.hl_bg), - // false => row, - // } }); - // self.table.select(self.major + self.minor + 1); self.table.scrollbar_state = self .table .scrollbar_state .content_length(cat.entries.len() + ctx.src_info.cats.len()); - // let last_elem = self.major + cat.entries.len() + 1; - // if last_elem > center.height - 2 { - // - // } - // *self.table.state.offset_mut() = 0; - // self.table.scrollbar_state = self.table.scrollbar_state.position(0); tbl.splice(self.major + 1..self.major + 1, cat_rows); let center = super::centered_rect(33, 14, area); - let clear = super::centered_rect(center.width + 2, center.height, area); - super::clear(clear, f.buffer_mut(), ctx.theme.bg); + // let clear = super::centered_rect(center.width + 2, center.height, area); + super::clear(center, f.buffer_mut(), ctx.theme.bg); let table = Table::new(tbl, [Constraint::Percentage(100)]) .block(border_block(&ctx.theme, true).title(title!("Category"))) .highlight_style(Style::default().bg(ctx.theme.hl_bg)); @@ -145,7 +134,6 @@ impl Widget for CategoryPopup { horizontal: 0, }); StatefulWidget::render(sb, sb_area, f.buffer_mut(), &mut self.table.scrollbar_state); - // .render(center, f.buffer_mut()); } } diff --git a/src/widget/clients.rs b/src/widget/clients.rs index 7b25345..9252031 100644 --- a/src/widget/clients.rs +++ b/src/widget/clients.rs @@ -29,14 +29,14 @@ impl Widget for ClientsPopup { fn draw(&mut self, f: &mut Frame, ctx: &Context, area: Rect) { let buf = f.buffer_mut(); let center = super::centered_rect(30, self.table.items.len() as u16 + 2, area); - let clear = super::centered_rect(center.width + 2, center.height, area); + // let clear = super::centered_rect(center.width + 2, center.height, area); let items = self.table.items.iter().map(|item| { Row::new(vec![match item == &ctx.client { true => format!("  {}", item), false => format!(" {}", item), }]) }); - super::clear(clear, buf, ctx.theme.bg); + super::clear(center, buf, ctx.theme.bg); let table = Table::new(items, [Constraint::Percentage(100)]) .block(border_block(&ctx.theme, true).title(title!("Download Client"))) .highlight_style(style!(bg:ctx.theme.hl_bg)); diff --git a/src/widget/error.rs b/src/widget/error.rs deleted file mode 100644 index 95a5098..0000000 --- a/src/widget/error.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::cmp::{max, min}; - -use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; -use ratatui::{ - layout::Rect, - style::Stylize, - widgets::{Paragraph, Widget as _, Wrap}, - Frame, -}; - -use crate::{ - app::{Context, Mode}, - title, -}; - -use super::{border_block, Widget}; - -pub struct ErrorPopup { - pub error: String, -} - -impl ErrorPopup { - pub fn with_error(&mut self, error: String) { - self.error = error; - } -} - -impl Default for ErrorPopup { - fn default() -> Self { - ErrorPopup { - error: "".to_owned(), - } - } -} - -impl Widget for ErrorPopup { - fn draw(&mut self, f: &mut Frame, ctx: &Context, area: Rect) { - let buf = f.buffer_mut(); - let lines = self.error.split('\n'); - let max_line = lines.clone().fold(30, |acc, e| max(e.len(), acc)) as u16 + 3; - let x_len = min(max_line, area.width - 4); - - // Get number of lines including wrapped lines - let height = lines.fold(0, |acc, e| { - acc + (e.len() as f32 / (x_len - 2) as f32).ceil() as u16 - }) + 2; - let center = super::centered_rect(x_len, height, area); - let clear = super::centered_rect(center.width + 2, center.height, area); - let p = Paragraph::new(self.error.to_owned()) - .block( - border_block(&ctx.theme, true) - .fg(ctx.theme.remake) - .title(title!( - "Error ({}): Press any key to dismiss", - ctx.errors.len() + 1 - )), - ) - .wrap(Wrap { trim: false }); - super::clear(clear, buf, ctx.theme.bg); - p.render(center, buf); - } - - fn handle_event(&mut self, ctx: &mut Context, e: &Event) { - if let Event::Key(KeyEvent { - code, - kind: KeyEventKind::Press, - .. - }) = e - { - match code { - KeyCode::Esc | KeyCode::Char(_) => { - if ctx.errors.is_empty() { - ctx.mode = Mode::Normal; - } - } - _ => {} - } - } - } - - fn get_help() -> Option> { - None - } -} diff --git a/src/widget/filter.rs b/src/widget/filter.rs index 769f7c0..0904dec 100644 --- a/src/widget/filter.rs +++ b/src/widget/filter.rs @@ -29,7 +29,7 @@ impl Default for FilterPopup { impl Widget for FilterPopup { fn draw(&mut self, f: &mut Frame, ctx: &Context, area: Rect) { let center = super::centered_rect(30, ctx.src_info.filters.len() as u16 + 2, area); - let clear = super::centered_rect(center.width + 2, center.height, area); + // let clear = super::centered_rect(center.width + 2, center.height, area); let items = ctx.src_info .filters @@ -39,7 +39,7 @@ impl Widget for FilterPopup { true => Row::new(vec![format!("  {}", item.to_owned())]), false => Row::new(vec![format!(" {}", item.to_owned())]), }); - super::clear(clear, f.buffer_mut(), ctx.theme.bg); + super::clear(center, f.buffer_mut(), ctx.theme.bg); Table::new(items, [Constraint::Percentage(100)]) .block(border_block(&ctx.theme, true).title(title!("Filter"))) .highlight_style(style!(bg:ctx.theme.hl_bg)) diff --git a/src/widget/help.rs b/src/widget/help.rs index 4d0cc11..c90db1a 100644 --- a/src/widget/help.rs +++ b/src/widget/help.rs @@ -51,7 +51,7 @@ impl Widget for HelpPopup { let height = min(max_size, self.table.items.len() + 3) as u16; let center = super::centered_rect(key_min + map_min + 6, height, area); - let clear = super::centered_rect(center.width + 2, center.height, area); + // let clear = super::centered_rect(center.width + 2, center.height, area); let items = self.table.items.iter().map(|(key, map)| { Row::new([ Line::from(key.to_string()).alignment(Alignment::Right), @@ -76,7 +76,7 @@ impl Widget for HelpPopup { .widths(Constraint::from_lengths([key_min, 1, map_min])) .highlight_style(style!(bg:ctx.theme.hl_bg)); - super::clear(clear, buf, ctx.theme.bg); + super::clear(center, buf, ctx.theme.bg); table.render(center, buf, &mut self.table.state); // Only show scrollbar if content overflows diff --git a/src/widget/notifications.rs b/src/widget/notifications.rs index 291cd5b..e902e42 100644 --- a/src/widget/notifications.rs +++ b/src/widget/notifications.rs @@ -1,5 +1,3 @@ -use std::ops::ControlFlow; - use crossterm::event::Event; use ratatui::{layout::Rect, Frame}; use serde::{Deserialize, Serialize}; @@ -27,8 +25,8 @@ impl Default for NotificationWidget { fn default() -> Self { Self { notifs: vec![], - duration: 5.0, - position: NotifyPosition::BottomRight, + duration: 3.0, + position: NotifyPosition::TopRight, } } } @@ -44,27 +42,22 @@ impl NotificationWidget { } pub fn add_notification(&mut self, notif: String) { - let mut new_notif = NotifyBox::new(notif, self.duration, self.position); - - self.notifs.sort_unstable_by_key(|a| a.offset()); - let first_gap = self.notifs.iter().try_fold(0, |prev, x| { - if x.offset() > prev { - ControlFlow::Break((prev, x.offset())) - } else { - ControlFlow::Continue(x.offset() + x.height()) - } - }); - let at_end = self - .notifs - .iter() - .last() - .map(|x| x.offset() + x.height()) - .unwrap_or(0); - let offset = match first_gap { - ControlFlow::Break((start, stop)) if stop >= new_notif.height() + start => start, - _ => at_end, - }; - new_notif.with_offset(offset); + let new_notif = NotifyBox::new(notif, self.duration, self.position, false); + + self.notifs + .iter_mut() + .for_each(|n| n.add_offset(new_notif.height())); + + self.notifs.push(new_notif); + } + + pub fn add_error(&mut self, error: String) { + let new_notif = NotifyBox::new(error, 0.0, self.position, true); + + self.notifs + .iter_mut() + .for_each(|n| n.add_offset(new_notif.height())); + self.notifs.push(new_notif); } @@ -73,20 +66,35 @@ impl NotificationWidget { } pub fn update(&mut self, deltatime: f64, area: Rect) -> bool { - let mut res = false; - for n in self.notifs.iter_mut() { - res = res || n.update(deltatime, area); - } - res + self.notifs.iter_mut().fold(false, |acc, x| { + let res = x.update(deltatime, area); + x.is_done() || res || acc + }) } } impl Widget for NotificationWidget { fn draw(&mut self, f: &mut Frame, ctx: &Context, area: Rect) { + let res = self + .notifs + .iter() + .filter_map(|n| match n.is_done() { + true => Some((n.offset(), n.height())), + false => None, + }) + .collect::>(); + for (offset, height) in res.iter() { + self.notifs.iter_mut().for_each(|n| { + if n.is_error() && n.offset() > *offset { + n.sub_offset(*height); + } + }) + } + self.notifs.retain(|n| !n.is_done()); + for n in self.notifs.iter_mut() { n.draw(f, ctx, area); } - self.notifs.retain(|n| !n.is_done()); } fn handle_event(&mut self, _ctx: &mut Context, _e: &Event) {} diff --git a/src/widget/notify_box.rs b/src/widget/notify_box.rs index 95f5cfc..8a6b428 100644 --- a/src/widget/notify_box.rs +++ b/src/widget/notify_box.rs @@ -33,63 +33,112 @@ impl NotifyPosition { area: Rect, width: u16, height: u16, - offset: u16, - ) -> ((i32, i32), (i32, i32)) { - let start_x = if self.is_left() { + start_offset: u16, + stop_offset: u16, + ) -> ((i32, i32), (i32, i32), (i32, i32)) { + let stop_x = if self.is_left() { area.left() as i32 - width as i32 } else { area.right() as i32 + 1 }; - let stop_x = if self.is_left() { + let start_x = if self.is_left() { area.left() as i32 + 2 } else { area.right() as i32 - width as i32 - 2 }; - // let start_y = if self.is_top() { - // (area.top() as i32 + 4) + offset as i32 - (height / 2) as i32 - // } else { - // (area.bottom() as i32 - 1) - offset as i32 - // }; + let start_y = if self.is_top() { + area.top() as i32 - height as i32 + start_offset as i32 + 1 + } else { + area.bottom() as i32 - start_offset as i32 - 1 + }; let stop_y = if self.is_top() { - (area.top() as i32 + 4) + offset as i32 + area.top() as i32 + stop_offset as i32 + 1 } else { - (area.bottom() as i32 - height as i32 - 1) - offset as i32 + area.bottom() as i32 - stop_offset as i32 - height as i32 - 1 }; - ((start_x, stop_y), (stop_x, stop_y)) + ((start_x, start_y), (start_x, stop_y), (stop_x, stop_y)) + } +} + +#[derive(Copy, Clone)] +pub struct AnimateState { + time: f64, + done: bool, +} + +impl AnimateState { + fn new() -> Self { + Self { + time: 0.0, + done: false, + } + } + + fn linear( + &mut self, + start_pos: (i32, i32), + stop_pos: (i32, i32), + rate: f64, + deltatime: f64, + ) -> (i32, i32) { + if self.time >= 1.0 { + self.done = true; + } + let pos = ( + ((self.time * (stop_pos.0 - start_pos.0) as f64) + start_pos.0 as f64).round() as i32, + ((self.time * (stop_pos.1 - start_pos.1) as f64) + start_pos.1 as f64).round() as i32, + ); + self.time = 1.0_f64.min(self.time + rate * deltatime); + pos + } + + fn is_done(self) -> bool { + self.done + } + + fn reset(&mut self) { + self.time = 0.0; + self.done = false; } } pub struct NotifyBox { + raw_content: String, content: String, pub time: f64, pub duration: f64, position: NotifyPosition, width: u16, height: u16, - offset: u16, + start_offset: u16, + stop_offset: u16, enter_state: AnimateState, leave_state: AnimateState, - pos: Option<(i32, i32)>, + pub pos: Option<(i32, i32)>, + error: bool, } impl NotifyBox { - pub fn new(content: String, duration: f64, position: NotifyPosition) -> Self { - let width = (MAX_WIDTH - 2).min(content.len() as u16); - let lines = textwrap::wrap(&content, width as usize); - let actual_width = lines.iter().fold(0, |acc, x| acc.max(x.len())) as u16; + pub fn new(content: String, duration: f64, position: NotifyPosition, error: bool) -> Self { + let raw_content = content.clone(); + let lines = textwrap::wrap(&content, MAX_WIDTH as usize); + let actual_width = lines.iter().fold(0, |acc, x| acc.max(x.len())) as u16 + 2; let content = lines.join("\n"); let height = lines.len() as u16 + 2; NotifyBox { - width: actual_width + 2, + width: actual_width, height, + raw_content, content, position, - offset: 0, + start_offset: 0, + stop_offset: 0, time: 0.0, duration, enter_state: AnimateState::new(), leave_state: AnimateState::new(), pos: None, + error, } } @@ -102,59 +151,31 @@ impl NotifyBox { } pub fn offset(&self) -> u16 { - self.offset + self.stop_offset } - pub fn with_offset(&mut self, offset: u16) -> &mut Self { - self.offset = offset; - self + pub fn is_done(&self) -> bool { + self.leave_state.is_done() } -} -#[derive(Copy, Clone)] -pub struct AnimateState { - time: f64, - done: bool, -} - -impl AnimateState { - fn new() -> Self { - Self { - time: 0.0, - done: false, - } + pub fn is_leaving(&self) -> bool { + self.time >= 1.0 } - fn linear( - &mut self, - start_pos: (i32, i32), - stop_pos: (i32, i32), - rate: f64, - deltatime: f64, - ) -> (i32, i32) { - if self.time >= 1.0 { - self.done = true; - } - let pos = ( - ((self.time * (stop_pos.0 - start_pos.0) as f64) + start_pos.0 as f64) as i32, - ((self.time * (stop_pos.1 - start_pos.1) as f64) + start_pos.1 as f64) as i32, - ); - self.time = 1.0_f64.min(self.time + rate * deltatime); - pos + pub fn is_error(&self) -> bool { + self.error } - fn is_done(self) -> bool { - self.done - } -} + pub fn add_offset(&mut self, offset: u16) { + self.enter_state.reset(); -impl NotifyBox { - pub fn is_done(&self) -> bool { - self.leave_state.is_done() + self.start_offset = self.stop_offset + self.height; + self.stop_offset += offset; } - pub fn is_leaving(&self) -> bool { - self.time >= 1.0 + pub fn sub_offset(&mut self, offset: u16) { + self.stop_offset = (self.stop_offset as i32 - offset as i32).max(0) as u16; + self.start_offset = (self.start_offset as i32 - offset as i32).max(0) as u16; } pub fn draw(&mut self, f: &mut Frame, ctx: &Context, area: Rect) { @@ -190,21 +211,35 @@ impl NotifyBox { } let scroll_x = (pos.0 + 1).min(0).unsigned_abs() as u16; let scroll_y = (pos.1 + 1).min(0).unsigned_abs() as u16; - let block = Block::new() - .border_style(style!(fg:ctx.theme.border_focused_color)) - .bg(ctx.theme.bg) - .fg(ctx.theme.fg) - .borders(border) - .border_type(ctx.theme.border); - - let clear = Rect::new( - (pos.0 - 1) as u16, - pos.1 as u16, - self.width + 2, - self.height, - ) - .intersection(area); - super::clear(clear, f.buffer_mut(), ctx.theme.bg); + let block = match self.error { + false => Block::new() + .border_style(style!(fg:ctx.theme.border_focused_color)) + .bg(ctx.theme.bg) + .fg(ctx.theme.fg) + .borders(border) + .border_type(ctx.theme.border), + true => { + let block = Block::new() + .border_style(style!(fg:ctx.theme.remake)) + .bg(ctx.theme.bg) + .fg(ctx.theme.remake) + .borders(border) + .border_type(ctx.theme.border); + match border.contains(Borders::TOP) { + true => block.title("Error: Press ESC to dismiss..."), + false => block, + } + } + }; + + // let clear = Rect::new( + // (pos.0 - 1).max(0) as u16, + // pos.1.max(0) as u16, + // self.width + 2, + // self.height, + // ) + // .intersection(area); + super::clear(rect, f.buffer_mut(), ctx.theme.bg); Paragraph::new(self.content.clone()) .block(block) .scroll((scroll_y, scroll_x)) @@ -212,23 +247,38 @@ impl NotifyBox { } fn next_pos(&mut self, deltatime: f64, area: Rect) -> (i32, i32) { - let (start_pos, stop_pos) = - self.position - .get_start_stop(area, self.width, self.height, self.offset); + let (start_pos, stop_pos, leave_pos) = self.position.get_start_stop( + area, + self.width, + self.height, + self.start_offset, + self.stop_offset, + ); match self.time >= 1.0 { false => self .enter_state .linear(start_pos, stop_pos, ANIM_SPEED, deltatime), true => self .leave_state - .linear(stop_pos, start_pos, ANIM_SPEED, deltatime), + .linear(stop_pos, leave_pos, ANIM_SPEED, deltatime), } } pub fn update(&mut self, deltatime: f64, area: Rect) -> bool { + let max_width = match self.error { + true => (area.width / 3).max(MAX_WIDTH), + false => area.width.min(MAX_WIDTH), + } as usize; + let lines = textwrap::wrap(&self.raw_content, max_width); + self.width = lines.iter().fold(0, |acc, x| acc.max(x.len())) as u16 + 2; + self.height = lines.len() as u16 + 2; + self.content = lines.join("\n"); + let last_pos = self.pos; self.pos = Some(self.next_pos(deltatime, area)); - if self.enter_state.is_done() { + + // Dont automatically dismiss errors + if self.enter_state.is_done() && !self.error { self.time = 1.0_f64.min(self.time + deltatime / self.duration); } last_pos != self.pos diff --git a/src/widget/page.rs b/src/widget/page.rs index be49ba4..9a2f10c 100644 --- a/src/widget/page.rs +++ b/src/widget/page.rs @@ -33,11 +33,11 @@ impl Widget for PagePopup { fn draw(&mut self, f: &mut Frame, ctx: &Context, area: Rect) { let buf = f.buffer_mut(); let center = super::centered_rect(13, 3, area); - let clear = super::centered_rect(center.width + 2, center.height, area); + // let clear = super::centered_rect(center.width + 2, center.height, area); let page_p = Paragraph::new(self.input.input.clone()); let indicator = Paragraph::new(">").block(border_block(&ctx.theme, true).title(title!("Goto Page"))); - super::clear(clear, buf, ctx.theme.bg); + super::clear(center, buf, ctx.theme.bg); indicator.render(center, buf); let input_area = center.inner(&Margin { diff --git a/src/widget/results.rs b/src/widget/results.rs index 920eceb..331e4f5 100644 --- a/src/widget/results.rs +++ b/src/widget/results.rs @@ -21,7 +21,6 @@ use super::{border_block, centered_rect, TitlePosition, VirtualStatefulTable}; pub struct ResultsWidget { pub table: VirtualStatefulTable, control_space: bool, - draw_count: usize, } impl ResultsWidget { @@ -43,7 +42,6 @@ impl Default for ResultsWidget { ResultsWidget { table: VirtualStatefulTable::new(), control_space: false, - draw_count: 0, } } } @@ -300,13 +298,6 @@ impl super::Widget for ResultsWidget { (Tab | BackTab, _) => { ctx.mode = Mode::Batch; } - (Char('b'), _) => { - ctx.notify("Test 1"); - ctx.notify("Test 2"); - ctx.notify("Test 3"); - } - // TODO: Dismiss popup notifs - // (Esc, &KeyModifiers::NONE) => { if self.control_space { ctx.notify("Exited VISUAL mode"); diff --git a/src/widget/sort.rs b/src/widget/sort.rs index 0c4d090..95c53d3 100644 --- a/src/widget/sort.rs +++ b/src/widget/sort.rs @@ -61,7 +61,7 @@ impl Widget for SortPopup { fn draw(&mut self, f: &mut Frame, ctx: &Context, area: Rect) { let buf = f.buffer_mut(); let center = super::centered_rect(30, ctx.src_info.sorts.len() as u16 + 2, area); - let clear = super::centered_rect(center.width + 2, center.height, area); + // let clear = super::centered_rect(center.width + 2, center.height, area); let items = ctx.src_info.sorts.iter().enumerate().map(|(i, item)| { Row::new([match i == self.selected.sort { true => format!("  {}", item), @@ -76,7 +76,7 @@ impl Widget for SortPopup { false => "Sort Descending", }))) .highlight_style(style!(bg:ctx.theme.hl_bg)); - super::clear(clear, buf, ctx.theme.bg); + super::clear(center, buf, ctx.theme.bg); table.render(center, buf, &mut self.table.state); } diff --git a/src/widget/sources.rs b/src/widget/sources.rs index d94e40f..b937c27 100644 --- a/src/widget/sources.rs +++ b/src/widget/sources.rs @@ -29,14 +29,14 @@ impl Widget for SourcesPopup { fn draw(&mut self, f: &mut Frame, ctx: &Context, area: Rect) { let buf = f.buffer_mut(); let center = super::centered_rect(30, self.table.items.len() as u16 + 2, area); - let clear = super::centered_rect(center.width + 2, center.height, area); + // let clear = super::centered_rect(center.width + 2, center.height, area); let items = self.table.items.iter().map(|item| { Row::new(vec![match item == &ctx.src { true => format!("  {}", item), false => format!(" {}", item), }]) }); - super::clear(clear, buf, ctx.theme.bg); + super::clear(center, buf, ctx.theme.bg); let table = Table::new(items, [Constraint::Percentage(100)]) .block(border_block(&ctx.theme, true).title(title!("Source"))) .highlight_style(style!(bg:ctx.theme.hl_bg)); diff --git a/src/widget/themes.rs b/src/widget/themes.rs index 043f0aa..1b3c8e1 100644 --- a/src/widget/themes.rs +++ b/src/widget/themes.rs @@ -33,7 +33,7 @@ impl Widget for ThemePopup { let buf = f.buffer_mut(); let height = min(min(ctx.themes.len() as u16 + 2, 10), area.height); let center = super::centered_rect(30, height, area); - let clear = super::centered_rect(center.width + 2, center.height, area); + // let clear = super::centered_rect(center.width + 2, center.height, area); let items = ctx.themes.keys().enumerate().map(|(i, item)| { Row::new(vec![ match i == self.selected { @@ -46,7 +46,7 @@ impl Widget for ThemePopup { let table = Table::new(items, [Constraint::Percentage(100)]) .block(border_block(&ctx.theme, true).title(title!("Theme"))) .highlight_style(style!(bg:ctx.theme.hl_bg)); - super::clear(clear, buf, ctx.theme.bg); + super::clear(center, buf, ctx.theme.bg); table.render(center, buf, &mut self.table.state); // Only show scrollbar if content overflows diff --git a/src/widget/user.rs b/src/widget/user.rs index d6be657..84b527e 100644 --- a/src/widget/user.rs +++ b/src/widget/user.rs @@ -31,11 +31,11 @@ impl Widget for UserPopup { fn draw(&mut self, f: &mut Frame, ctx: &Context, area: Rect) { let buf = f.buffer_mut(); let center = super::centered_rect(30, 3, area); - let clear = super::centered_rect(center.width + 2, center.height, area); + // let clear = super::centered_rect(center.width + 2, center.height, area); let page_p = Paragraph::new(self.input.input.clone()); let indicator = Paragraph::new(">") .block(border_block(&ctx.theme, true).title(title!("Posts by User"))); - super::clear(clear, buf, ctx.theme.bg); + super::clear(center, buf, ctx.theme.bg); indicator.render(center, buf); let input_area = center.inner(&Margin {