From 774198933c31fedf38ca6211386a5ea29427cfa7 Mon Sep 17 00:00:00 2001 From: Amanda Graven Date: Thu, 3 Dec 2020 12:46:44 +0100 Subject: [PATCH] Restructure ui into view structs --- src/ui.rs | 592 ++++++++++++++++++++++++++---------------------------- 1 file changed, 289 insertions(+), 303 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index cef40bc..0ea0f50 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -15,52 +15,258 @@ use matrix_sdk::{ use crate::matrix; -#[derive(Debug, Clone)] -pub enum Retrix { - Prompt { - user_input: text_input::State, - password_input: text_input::State, - server_input: text_input::State, - login_button: iced::button::State, +/// View for the login prompt +#[derive(Debug, Clone, Default)] +pub struct PromptView { + user_input: text_input::State, + password_input: text_input::State, + server_input: text_input::State, + login_button: iced::button::State, - user: String, - password: String, - server: String, - action: PromptAction, - error: Option, - }, - AwaitLogin(std::time::Instant), - LoggedIn { - client: matrix_sdk::Client, - session: matrix::Session, - - rooms: BTreeMap, - buttons: HashMap, - messages: BTreeMap, - selected: Option, - room_scroll: iced::scrollable::State, - message_scroll: iced::scrollable::State, - message_input: iced::text_input::State, - draft: String, - send_button: iced::button::State, - }, + user: String, + password: String, + server: String, + action: PromptAction, + error: Option, } -impl Retrix { - pub fn new_prompt() -> Retrix { - Retrix::Prompt { - user_input: text_input::State::new(), - password_input: text_input::State::new(), - server_input: text_input::State::new(), - login_button: Default::default(), +impl PromptView { + pub fn new() -> Self { + Self::default() + } - user: String::new(), - password: String::new(), - server: String::new(), - action: PromptAction::Login, - error: None, + pub fn view(&mut self) -> Element { + let mut content = Column::new() + .width(500.into()) + .push( + Row::new() + .spacing(15) + .push(Radio::new( + PromptAction::Login, + "Login", + Some(self.action), + Message::SetAction, + )) + .push(Radio::new( + PromptAction::Signup, + "Sign up", + Some(self.action), + Message::SetAction, + )), + ) + .push(Text::new("Username")) + .push( + TextInput::new( + &mut self.user_input, + "Username", + &self.user, + Message::SetUser, + ) + .padding(5), + ) + .push(Text::new("Password")) + .push( + TextInput::new( + &mut self.password_input, + "Password", + &self.password, + Message::SetPassword, + ) + .password() + .padding(5), + ) + .push(Text::new("Homeserver")) + .push( + TextInput::new( + &mut self.server_input, + "Server", + &self.server, + Message::SetServer, + ) + .padding(5), + ); + let button = match self.action { + PromptAction::Login => { + Button::new(&mut self.login_button, Text::new("Login")).on_press(Message::Login) + } + PromptAction::Signup => { + content = content.push( + Text::new("NB: Signup is very naively implemented, and prone to breaking") + .color([0.2, 0.2, 0.0]), + ); + Button::new(&mut self.login_button, Text::new("Sign up")).on_press(Message::Signup) + } + }; + content = content.push(button); + if let Some(ref error) = self.error { + content = content.push(Text::new(error).color([1.0, 0.0, 0.0])); + } + + Container::new(content) + .center_x() + .center_y() + .width(iced::Length::Fill) + .height(iced::Length::Fill) + .into() + } +} + +/// Main view after successful login +#[derive(Debug, Clone)] +pub struct MainView { + client: matrix_sdk::Client, + session: matrix::Session, + + rooms: BTreeMap, + buttons: HashMap, + messages: BTreeMap, + selected: Option, + room_scroll: iced::scrollable::State, + message_scroll: iced::scrollable::State, + message_input: iced::text_input::State, + draft: String, + send_button: iced::button::State, +} + +impl MainView { + pub fn new(client: matrix_sdk::Client, session: matrix::Session) -> Self { + Self { + client, + session, + rooms: Default::default(), + selected: None, + room_scroll: Default::default(), + message_scroll: Default::default(), + message_input: Default::default(), + buttons: Default::default(), + messages: Default::default(), + draft: String::new(), + send_button: Default::default(), } } + + pub fn view(&mut self) -> Element { + let mut root_row = Row::new().width(Length::Fill).height(Length::Fill); + + // Room list + let joined = self.client.joined_rooms(); + let rooms = futures::executor::block_on(async { joined.read().await }); + let mut room_col = Scrollable::new(&mut self.room_scroll) + .width(400.into()) + .height(Length::Fill) + .scrollbar_width(5); + // We have to iterate the buttons map and not the other way around to make the + // borrow checker happy. First we make sure there's a button entry for every room + // entry, and clean up button entries from removed rooms. + for (id, _) in rooms.iter() { + self.buttons.entry(id.to_owned()).or_default(); + } + self.buttons.retain(|id, _| rooms.contains_key(id)); + // Then we make our buttons + let buttons: Vec> = self + .buttons + .iter_mut() + .map(|(id, state)| { + // Get read lock for the room + let room = + futures::executor::block_on(async { rooms.get(id).unwrap().read().await }); + Button::new(state, Text::new(room.display_name())) + .on_press(Message::SelectRoom(id.to_owned())) + .width(400.into()) + }) + .collect(); + // Then we add them to our room column. What a mess. + for button in buttons { + room_col = room_col.push(button); + } + root_row = root_row.push(room_col); + + // Messages. + // + // Get selected room. + let selected_room = self.selected.as_ref().and_then(|selected| { + futures::executor::block_on(async { + match rooms.get(selected) { + Some(room) => Some(room.read().await), + None => None, + } + }) + }); + if let Some(room) = selected_room { + let mut col = Column::new() + .spacing(5) + .padding(5) + .push(Text::new(room.display_name()).size(25)) + .push(Rule::horizontal(2)); + let mut scroll = Scrollable::new(&mut self.message_scroll) + .scrollbar_width(2) + .height(Length::Fill); + for message in room.messages.iter() { + if let AnyPossiblyRedactedSyncMessageEvent::Regular(event) = message { + if let AnySyncMessageEvent::RoomMessage(room_message) = event { + match &room_message.content { + MessageEventContent::Text(text) => { + let row = Row::new() + .spacing(5) + .push( + // Render senders disambiguated name or fallback to + // mxid + Text::new( + room.joined_members + .get(&room_message.sender) + .map(|sender| sender.disambiguated_name()) + .unwrap_or(room_message.sender.to_string()), + ) + .color([0.2, 0.2, 1.0]), + ) + .push(Text::new(&text.body).width(Length::Fill)) + .push(Text::new(format_systime(room_message.origin_server_ts))); + scroll = scroll.push(row); + } + _ => (), + } + } + } + } + col = col.push(scroll).push( + Row::new() + .push( + TextInput::new( + &mut self.message_input, + "Write a message...", + &self.draft, + Message::SetMessage, + ) + .width(Length::Fill) + .padding(5) + .on_submit(Message::SendMessage), + ) + .push( + Button::new(&mut self.send_button, Text::new("Send")) + .on_press(Message::SendMessage), + ), + ); + + root_row = root_row.push(col); + } else { + root_row = root_row.push( + Container::new(Text::new("Select a room to start chatting")) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill), + ); + } + + root_row.into() + } +} + +#[derive(Debug, Clone)] +pub enum Retrix { + Prompt(PromptView), + AwaitLogin, + LoggedIn(MainView), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -69,6 +275,12 @@ pub enum PromptAction { Signup, } +impl Default for PromptAction { + fn default() -> Self { + PromptAction::Login + } +} + #[derive(Debug, Clone)] pub enum Message { // Login form messages @@ -106,9 +318,9 @@ impl Application for Retrix { Err(e) => Message::LoginFailed(e.to_string()), }, ); - (Retrix::AwaitLogin(std::time::Instant::now()), command) + (Retrix::AwaitLogin, command) } - None => (Retrix::new_prompt(), Command::none()), + None => (Retrix::Prompt(PromptView::new()), Command::none()), } } @@ -118,8 +330,8 @@ impl Application for Retrix { fn subscription(&self) -> Subscription { match self { - Retrix::LoggedIn { client, .. } => { - matrix::MatrixSync::subscription(client.clone()).map(Message::Sync) + Retrix::LoggedIn(view) => { + matrix::MatrixSync::subscription(view.client.clone()).map(Message::Sync) } _ => Subscription::none(), } @@ -127,22 +339,16 @@ impl Application for Retrix { fn update(&mut self, message: Self::Message) -> Command { match self { - Retrix::Prompt { - user, - password, - server, - action, - .. - } => match message { - Message::SetUser(u) => *user = u, - Message::SetPassword(p) => *password = p, - Message::SetServer(s) => *server = s, - Message::SetAction(a) => *action = a, + Retrix::Prompt(prompt) => match message { + Message::SetUser(u) => prompt.user = u, + Message::SetPassword(p) => prompt.password = p, + Message::SetServer(s) => prompt.server = s, + Message::SetAction(a) => prompt.action = a, Message::Login => { - let user = user.clone(); - let password = password.clone(); - let server = server.clone(); - *self = Retrix::AwaitLogin(std::time::Instant::now()); + let user = prompt.user.clone(); + let password = prompt.password.clone(); + let server = prompt.server.clone(); + *self = Retrix::AwaitLogin; return Command::perform( async move { matrix::login(&user, &password, &server).await }, |result| match result { @@ -152,10 +358,10 @@ impl Application for Retrix { ); } Message::Signup => { - let user = user.clone(); - let password = password.clone(); - let server = server.clone(); - *self = Retrix::AwaitLogin(std::time::Instant::now()); + let user = prompt.user.clone(); + let password = prompt.password.clone(); + let server = prompt.server.clone(); + *self = Retrix::AwaitLogin; return Command::perform( async move { matrix::signup(&user, &password, &server).await }, |result| match result { @@ -166,27 +372,14 @@ impl Application for Retrix { } _ => (), }, - Retrix::AwaitLogin(_) => match message { + Retrix::AwaitLogin => match message { Message::LoginFailed(e) => { - *self = Retrix::new_prompt(); - if let Retrix::Prompt { ref mut error, .. } = *self { - *error = Some(e); - } + let mut view = PromptView::default(); + view.error = Some(e); + *self = Retrix::Prompt(view); } Message::LoggedIn(client, session) => { - *self = Retrix::LoggedIn { - client: client.clone(), - session, - rooms: BTreeMap::new(), - selected: None, - room_scroll: Default::default(), - message_scroll: Default::default(), - message_input: Default::default(), - buttons: Default::default(), - messages: Default::default(), - draft: String::new(), - send_button: Default::default(), - }; + *self = Retrix::LoggedIn(MainView::new(client, session)); /*let client = client.clone(); return Command::perform( async move { @@ -202,26 +395,20 @@ impl Application for Retrix { } _ => (), }, - Retrix::LoggedIn { - rooms, - selected, - draft, - client, - .. - } => match message { - Message::ResetRooms(r) => *rooms = r, - Message::SelectRoom(r) => *selected = Some(r), + Retrix::LoggedIn(view) => match message { + Message::ResetRooms(r) => view.rooms = r, + Message::SelectRoom(r) => view.selected = Some(r), Message::Sync(event) => match event { AnyRoomEvent::Message(_message) => (), AnyRoomEvent::State(_state) => (), AnyRoomEvent::RedactedMessage(_message) => (), AnyRoomEvent::RedactedState(_state) => (), }, - Message::SetMessage(m) => *draft = m, + Message::SetMessage(m) => view.draft = m, Message::SendMessage => { - let selected = selected.to_owned(); - let draft = draft.clone(); - let client = client.clone(); + let selected = view.selected.to_owned(); + let draft = view.draft.clone(); + let client = view.client.clone(); return Command::perform( async move { client @@ -248,215 +435,14 @@ impl Application for Retrix { fn view(&mut self) -> Element { match self { - Retrix::Prompt { - user_input, - password_input, - server_input, - login_button, - user, - password, - server, - action, - error, - } => { - // Login form - let mut content = Column::new() - .width(500.into()) - .push( - Row::new() - .push(Radio::new( - PromptAction::Login, - "Login", - Some(*action), - Message::SetAction, - )) - .push(Radio::new( - PromptAction::Signup, - "Sign up", - Some(*action), - Message::SetAction, - )), - ) - .push(Text::new("Username")) - .push(TextInput::new(user_input, "Username", user, Message::SetUser).padding(5)) - .push(Text::new("Password")) - .push( - TextInput::new(password_input, "Password", password, Message::SetPassword) - .password() - .padding(5), - ) - .push(Text::new("Homeserver")) - .push( - TextInput::new(server_input, "Server", server, Message::SetServer) - .padding(5), - ); - let button = match *action { - PromptAction::Login => { - Button::new(login_button, Text::new("Login")).on_press(Message::Login) - } - PromptAction::Signup => { - content = content.push( - Text::new( - "NB: Signup is very naively implemented, and prone to breaking", - ) - .color([0.2, 0.2, 0.0]), - ); - Button::new(login_button, Text::new("Sign up")).on_press(Message::Signup) - } - }; - content = content.push(button); - if let Some(ref error) = error { - content = content.push(Text::new(error).color([1.0, 0.0, 0.0])); - } - - Container::new(content) - .center_x() - .center_y() - .width(iced::Length::Fill) - .height(iced::Length::Fill) - .into() - } - Retrix::AwaitLogin(instant) => Container::new(Text::new(format!( - "Logging in{}", - match instant.elapsed().subsec_millis() / 333 { - 0 => ".", - 1 => "..", - 2 => "...", - _ => "....", - } - ))) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) - .into(), - Retrix::LoggedIn { - client, - room_scroll, - message_scroll, - message_input, - send_button, - buttons, - selected, - draft, - .. - } => { - let mut root_row = Row::new().width(Length::Fill).height(Length::Fill); - - // Room list - let joined = client.joined_rooms(); - let rooms = futures::executor::block_on(async { joined.read().await }); - let mut room_col = Scrollable::new(room_scroll) - .width(400.into()) - .height(Length::Fill) - .scrollbar_width(5); - // We have to iterate the buttons map and not the other way around to make the - // borrow checker happy. First we make sure there's a button entry for every room - // entry, and clean up button entries from removed rooms. - for (id, _) in rooms.iter() { - buttons.entry(id.to_owned()).or_default(); - } - buttons.retain(|id, _| rooms.contains_key(id)); - // Then we make our buttons - let buttons: Vec> = buttons - .iter_mut() - .map(|(id, state)| { - // Get read lock for the room - let room = futures::executor::block_on(async { - rooms.get(id).unwrap().read().await - }); - Button::new(state, Text::new(room.display_name())) - .on_press(Message::SelectRoom(id.to_owned())) - .width(400.into()) - }) - .collect(); - // Then we add them to our room column. What a mess. - for button in buttons { - room_col = room_col.push(button); - } - root_row = root_row.push(room_col); - - // Messages. - // - // Get selected room. - let selected_room = selected.as_ref().and_then(|selected| { - futures::executor::block_on(async { - match rooms.get(selected) { - Some(room) => Some(room.read().await), - None => None, - } - }) - }); - if let Some(room) = selected_room { - let mut col = Column::new() - .spacing(5) - .padding(5) - .push(Text::new(room.display_name()).size(25)) - .push(Rule::horizontal(2)); - let mut scroll = Scrollable::new(message_scroll) - .scrollbar_width(2) - .height(Length::Fill); - for message in room.messages.iter() { - if let AnyPossiblyRedactedSyncMessageEvent::Regular(event) = message { - if let AnySyncMessageEvent::RoomMessage(room_message) = event { - match &room_message.content { - MessageEventContent::Text(text) => { - let row = Row::new() - .spacing(5) - .push( - // Render senders disambiguated name or fallback to - // mxid - Text::new( - room.joined_members - .get(&room_message.sender) - .map(|sender| sender.disambiguated_name()) - .unwrap_or(room_message.sender.to_string()), - ) - .color([0.2, 0.2, 1.0]), - ) - .push(Text::new(&text.body).width(Length::Fill)) - .push(Text::new(format_systime( - room_message.origin_server_ts, - ))); - scroll = scroll.push(row); - } - _ => (), - } - } - } - } - col = col.push(scroll).push( - Row::new() - .push( - TextInput::new( - message_input, - "Write a message...", - draft, - Message::SetMessage, - ) - .width(Length::Fill) - .padding(5) - .on_submit(Message::SendMessage), - ) - .push( - Button::new(send_button, Text::new("Send")) - .on_press(Message::SendMessage), - ), - ); - - root_row = root_row.push(col); - } else { - root_row = root_row.push( - Container::new(Text::new("Select a room to start chatting")) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill), - ); - } - - root_row.into() - } + Retrix::Prompt(prompt) => prompt.view(), + Retrix::AwaitLogin => Container::new(Text::new(format!("Logging in..."))) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill) + .into(), + Retrix::LoggedIn(view) => view.view(), } } }