Add error popups to room UI
This commit is contained in:
parent
ded927b9f0
commit
ab36df3c2b
4 changed files with 96 additions and 18 deletions
|
|
@ -1 +1,2 @@
|
||||||
|
mod popup;
|
||||||
pub mod room;
|
pub mod room;
|
||||||
|
|
|
||||||
40
src/ui/euph/popup.rs
Normal file
40
src/ui/euph/popup.rs
Normal file
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
use crossterm::style::{Color, ContentStyle, Stylize};
|
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 euphoxide::conn::{Joined, Status};
|
||||||
use parking_lot::FairMutex;
|
use parking_lot::FairMutex;
|
||||||
use tokio::sync::oneshot::error::TryRecvError;
|
use tokio::sync::oneshot::error::TryRecvError;
|
||||||
|
|
@ -30,6 +31,8 @@ use crate::ui::widgets::BoxedWidget;
|
||||||
use crate::ui::{util, UiEvent};
|
use crate::ui::{util, UiEvent};
|
||||||
use crate::vault::EuphVault;
|
use crate::vault::EuphVault;
|
||||||
|
|
||||||
|
use super::popup::Popup;
|
||||||
|
|
||||||
enum State {
|
enum State {
|
||||||
Normal,
|
Normal,
|
||||||
ChooseNick(EditorState),
|
ChooseNick(EditorState),
|
||||||
|
|
@ -42,6 +45,7 @@ pub struct EuphRoom {
|
||||||
room: Option<euph::Room>,
|
room: Option<euph::Room>,
|
||||||
|
|
||||||
state: State,
|
state: State,
|
||||||
|
popups: VecDeque<Popup>,
|
||||||
|
|
||||||
chat: ChatState<euph::SmallMessage, EuphVault>,
|
chat: ChatState<euph::SmallMessage, EuphVault>,
|
||||||
last_msg_sent: Option<oneshot::Receiver<Snowflake>>,
|
last_msg_sent: Option<oneshot::Receiver<Snowflake>>,
|
||||||
|
|
@ -56,6 +60,7 @@ impl EuphRoom {
|
||||||
vault: vault.clone(),
|
vault: vault.clone(),
|
||||||
room: None,
|
room: None,
|
||||||
state: State::Normal,
|
state: State::Normal,
|
||||||
|
popups: VecDeque::new(),
|
||||||
chat: ChatState::new(vault),
|
chat: ChatState::new(vault),
|
||||||
last_msg_sent: None,
|
last_msg_sent: None,
|
||||||
nick_list: ListState::new(),
|
nick_list: ListState::new(),
|
||||||
|
|
@ -145,10 +150,12 @@ impl EuphRoom {
|
||||||
Some(Some(Status::Joined(joined))) => self.widget_with_nick_list(&status, joined).await,
|
Some(Some(Status::Joined(joined))) => self.widget_with_nick_list(&status, joined).await,
|
||||||
_ => self.widget_without_nick_list(&status).await,
|
_ => self.widget_without_nick_list(&status).await,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut layers = vec![chat];
|
||||||
|
|
||||||
match &self.state {
|
match &self.state {
|
||||||
State::Normal => chat,
|
State::Normal => {}
|
||||||
State::ChooseNick(ed) => Layer::new(vec![
|
State::ChooseNick(ed) => layers.push(
|
||||||
chat,
|
|
||||||
Float::new(Border::new(Background::new(VJoin::new(vec![
|
Float::new(Border::new(Background::new(VJoin::new(vec![
|
||||||
Segment::new(Padding::new(Text::new("Choose nick")).horizontal(1)),
|
Segment::new(Padding::new(Text::new("Choose nick")).horizontal(1)),
|
||||||
Segment::new(
|
Segment::new(
|
||||||
|
|
@ -162,9 +169,14 @@ impl EuphRoom {
|
||||||
.horizontal(0.5)
|
.horizontal(0.5)
|
||||||
.vertical(0.5)
|
.vertical(0.5)
|
||||||
.into(),
|
.into(),
|
||||||
])
|
),
|
||||||
.into(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for popup in &self.popups {
|
||||||
|
layers.push(popup.widget());
|
||||||
|
}
|
||||||
|
|
||||||
|
Layer::new(layers).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn widget_without_nick_list(&self, status: &Option<Option<Status>>) -> BoxedWidget {
|
async fn widget_without_nick_list(&self, status: &Option<Option<Status>>) -> BoxedWidget {
|
||||||
|
|
@ -350,6 +362,11 @@ impl EuphRoom {
|
||||||
pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
||||||
bindings.heading("Room");
|
bindings.heading("Room");
|
||||||
|
|
||||||
|
if !self.popups.is_empty() {
|
||||||
|
bindings.binding("esc", "close popup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
match &self.state {
|
match &self.state {
|
||||||
State::Normal => {
|
State::Normal => {
|
||||||
// TODO Use if-let chain
|
// TODO Use if-let chain
|
||||||
|
|
@ -382,6 +399,14 @@ impl EuphRoom {
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
crossterm_lock: &Arc<FairMutex<()>>,
|
||||||
event: &InputEvent,
|
event: &InputEvent,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
if !self.popups.is_empty() {
|
||||||
|
if matches!(event, key!(Esc)) {
|
||||||
|
self.popups.pop_back();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
match &self.state {
|
match &self.state {
|
||||||
State::Normal => {
|
State::Normal => {
|
||||||
// TODO Use if-let chain
|
// TODO Use if-let chain
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,25 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use crossterm::style::ContentStyle;
|
||||||
use toss::frame::{Frame, Pos, Size};
|
use toss::frame::{Frame, Pos, Size};
|
||||||
|
|
||||||
use super::{BoxedWidget, Widget};
|
use super::{BoxedWidget, Widget};
|
||||||
|
|
||||||
pub struct Border(BoxedWidget);
|
pub struct Border {
|
||||||
|
inner: BoxedWidget,
|
||||||
|
style: ContentStyle,
|
||||||
|
}
|
||||||
|
|
||||||
impl Border {
|
impl Border {
|
||||||
pub fn new<W: Into<BoxedWidget>>(inner: W) -> Self {
|
pub fn new<W: Into<BoxedWidget>>(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<u16>, max_height: Option<u16>) -> Size {
|
fn size(&self, frame: &mut Frame, max_width: Option<u16>, max_height: Option<u16>) -> Size {
|
||||||
let max_width = max_width.map(|w| w.saturating_sub(2));
|
let max_width = max_width.map(|w| w.saturating_sub(2));
|
||||||
let max_height = max_height.map(|h| h.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)
|
size + Size::new(2, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,23 +39,23 @@ impl Widget for Border {
|
||||||
|
|
||||||
let right = size.width as i32 - 1;
|
let right = size.width as i32 - 1;
|
||||||
let bottom = size.height as i32 - 1;
|
let bottom = size.height as i32 - 1;
|
||||||
frame.write(Pos::new(0, 0), "┌");
|
frame.write(Pos::new(0, 0), ("┌", self.style));
|
||||||
frame.write(Pos::new(right, 0), "┐");
|
frame.write(Pos::new(right, 0), ("┐", self.style));
|
||||||
frame.write(Pos::new(0, bottom), "└");
|
frame.write(Pos::new(0, bottom), ("└", self.style));
|
||||||
frame.write(Pos::new(right, bottom), "┘");
|
frame.write(Pos::new(right, bottom), ("┘", self.style));
|
||||||
|
|
||||||
for y in 1..bottom {
|
for y in 1..bottom {
|
||||||
frame.write(Pos::new(0, y), "│");
|
frame.write(Pos::new(0, y), ("│", self.style));
|
||||||
frame.write(Pos::new(right, y), "│");
|
frame.write(Pos::new(right, y), ("│", self.style));
|
||||||
}
|
}
|
||||||
|
|
||||||
for x in 1..right {
|
for x in 1..right {
|
||||||
frame.write(Pos::new(x, 0), "─");
|
frame.write(Pos::new(x, 0), ("─", self.style));
|
||||||
frame.write(Pos::new(x, bottom), "─");
|
frame.write(Pos::new(x, bottom), ("─", self.style));
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.push(Pos::new(1, 1), size - Size::new(2, 2));
|
frame.push(Pos::new(1, 1), size - Size::new(2, 2));
|
||||||
self.0.render(frame).await;
|
self.inner.render(frame).await;
|
||||||
frame.pop();
|
frame.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue