diff --git a/src/ui/euph.rs b/src/ui/euph.rs index addf7a5..0036d78 100644 --- a/src/ui/euph.rs +++ b/src/ui/euph.rs @@ -1 +1,2 @@ +mod popup; pub mod room; diff --git a/src/ui/euph/popup.rs b/src/ui/euph/popup.rs new file mode 100644 index 0000000..901b29d --- /dev/null +++ b/src/ui/euph/popup.rs @@ -0,0 +1,40 @@ +use crossterm::style::{ContentStyle, Stylize}; +use toss::styled::Styled; + +use crate::ui::widgets::background::Background; +use crate::ui::widgets::border::Border; +use crate::ui::widgets::layer::Layer; +use crate::ui::widgets::padding::Padding; +use crate::ui::widgets::text::Text; +use crate::ui::widgets::BoxedWidget; + +pub enum Popup { + ServerError { description: String, reason: String }, +} + +impl Popup { + fn server_error_widget(description: &str, reason: &str) -> BoxedWidget { + let border_style = ContentStyle::default().red().bold(); + let text = Styled::new_plain(description) + .then_plain("\n\n") + .then_plain(reason); + Layer::new(vec![ + Padding::new(Text::new(("Error", border_style))) + .horizontal(1) + .into(), + Border::new(Background::new(Text::new(text))) + .style(border_style) + .into(), + ]) + .into() + } + + pub fn widget(&self) -> BoxedWidget { + match self { + Self::ServerError { + description, + reason, + } => Self::server_error_widget(description, reason), + } + } +} diff --git a/src/ui/euph/room.rs b/src/ui/euph/room.rs index 37a499d..fe05c16 100644 --- a/src/ui/euph/room.rs +++ b/src/ui/euph/room.rs @@ -1,9 +1,10 @@ +use std::collections::VecDeque; use std::iter; use std::sync::Arc; use crossterm::event::KeyCode; use crossterm::style::{Color, ContentStyle, Stylize}; -use euphoxide::api::{SessionType, SessionView, Snowflake}; +use euphoxide::api::{Data, SessionType, SessionView, Snowflake}; use euphoxide::conn::{Joined, Status}; use parking_lot::FairMutex; use tokio::sync::oneshot::error::TryRecvError; @@ -30,6 +31,8 @@ use crate::ui::widgets::BoxedWidget; use crate::ui::{util, UiEvent}; use crate::vault::EuphVault; +use super::popup::Popup; + enum State { Normal, ChooseNick(EditorState), @@ -42,6 +45,7 @@ pub struct EuphRoom { room: Option, state: State, + popups: VecDeque, chat: ChatState, last_msg_sent: Option>, @@ -56,6 +60,7 @@ impl EuphRoom { vault: vault.clone(), room: None, state: State::Normal, + popups: VecDeque::new(), chat: ChatState::new(vault), last_msg_sent: None, nick_list: ListState::new(), @@ -145,10 +150,12 @@ impl EuphRoom { Some(Some(Status::Joined(joined))) => self.widget_with_nick_list(&status, joined).await, _ => self.widget_without_nick_list(&status).await, }; + + let mut layers = vec![chat]; + match &self.state { - State::Normal => chat, - State::ChooseNick(ed) => Layer::new(vec![ - chat, + State::Normal => {} + State::ChooseNick(ed) => layers.push( Float::new(Border::new(Background::new(VJoin::new(vec![ Segment::new(Padding::new(Text::new("Choose nick")).horizontal(1)), Segment::new( @@ -162,9 +169,14 @@ impl EuphRoom { .horizontal(0.5) .vertical(0.5) .into(), - ]) - .into(), + ), } + + for popup in &self.popups { + layers.push(popup.widget()); + } + + Layer::new(layers).into() } async fn widget_without_nick_list(&self, status: &Option>) -> BoxedWidget { @@ -350,6 +362,11 @@ impl EuphRoom { pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList) { bindings.heading("Room"); + if !self.popups.is_empty() { + bindings.binding("esc", "close popup"); + return; + } + match &self.state { State::Normal => { // TODO Use if-let chain @@ -382,6 +399,14 @@ impl EuphRoom { crossterm_lock: &Arc>, event: &InputEvent, ) -> bool { + if !self.popups.is_empty() { + if matches!(event, key!(Esc)) { + self.popups.pop_back(); + return true; + } + return false; + } + match &self.state { State::Normal => { // TODO Use if-let chain diff --git a/src/ui/widgets/border.rs b/src/ui/widgets/border.rs index cb18090..fd32a9c 100644 --- a/src/ui/widgets/border.rs +++ b/src/ui/widgets/border.rs @@ -1,13 +1,25 @@ use async_trait::async_trait; +use crossterm::style::ContentStyle; use toss::frame::{Frame, Pos, Size}; use super::{BoxedWidget, Widget}; -pub struct Border(BoxedWidget); +pub struct Border { + inner: BoxedWidget, + style: ContentStyle, +} impl Border { pub fn new>(inner: W) -> Self { - Self(inner.into()) + Self { + inner: inner.into(), + style: ContentStyle::default(), + } + } + + pub fn style(mut self, style: ContentStyle) -> Self { + self.style = style; + self } } @@ -16,7 +28,7 @@ impl Widget for Border { fn size(&self, frame: &mut Frame, max_width: Option, max_height: Option) -> Size { let max_width = max_width.map(|w| w.saturating_sub(2)); let max_height = max_height.map(|h| h.saturating_sub(2)); - let size = self.0.size(frame, max_width, max_height); + let size = self.inner.size(frame, max_width, max_height); size + Size::new(2, 2) } @@ -27,23 +39,23 @@ impl Widget for Border { let right = size.width as i32 - 1; let bottom = size.height as i32 - 1; - frame.write(Pos::new(0, 0), "┌"); - frame.write(Pos::new(right, 0), "┐"); - frame.write(Pos::new(0, bottom), "└"); - frame.write(Pos::new(right, bottom), "┘"); + frame.write(Pos::new(0, 0), ("┌", self.style)); + frame.write(Pos::new(right, 0), ("┐", self.style)); + frame.write(Pos::new(0, bottom), ("└", self.style)); + frame.write(Pos::new(right, bottom), ("┘", self.style)); for y in 1..bottom { - frame.write(Pos::new(0, y), "│"); - frame.write(Pos::new(right, y), "│"); + frame.write(Pos::new(0, y), ("│", self.style)); + frame.write(Pos::new(right, y), ("│", self.style)); } for x in 1..right { - frame.write(Pos::new(x, 0), "─"); - frame.write(Pos::new(x, bottom), "─"); + frame.write(Pos::new(x, 0), ("─", self.style)); + frame.write(Pos::new(x, bottom), ("─", self.style)); } frame.push(Pos::new(1, 1), size - Size::new(2, 2)); - self.0.render(frame).await; + self.inner.render(frame).await; frame.pop(); } }