diff --git a/src/main.rs b/src/main.rs index 999d626..d6d8045 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,21 @@ mod irc_message; mod message_log; -use crate::irc_message::IrcMessage; -use crate::message_log::MessageLog; -use std::cell::RefCell; - +use crate::{irc_message::IrcMessage, message_log::MessageLog}; use color_eyre::eyre::Result; use futures::StreamExt; -use iced::alignment::Horizontal; -use iced::theme::Theme; -use iced::widget::{column, container, mouse_area, row, scrollable, text, text_input}; -use iced::{executor, Application, Background, Color, Length, Settings}; +use iced::{ + executor, + theme::Theme, + widget::{column, container, mouse_area, row, text, text_input}, + Application, Background, Color, Length, Settings, +}; use once_cell::sync::Lazy; -use tokio::select; -use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use std::cell::RefCell; +use tokio::{ + select, + sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, +}; static INPUT_ID: Lazy = Lazy::new(text_input::Id::unique); @@ -77,7 +79,7 @@ struct CriFlags { } #[derive(Debug, Clone)] -enum UiMessage { +pub enum UiMessage { IrcMessageReceived(Box), InputChanged(String), InputSubmitted, @@ -195,142 +197,10 @@ impl Application for Cri { } fn view(&self) -> iced::Element<'_, Self::Message, iced::Renderer> { - let lighter_grey = Color::new(0.93, 0.94, 0.95, 1.0); let dark_grey = Color::new(0.58, 0.65, 0.65, 1.0); - let lighter_green = Color::new(0.94, 0.99, 0.87, 1.0); - let dark_red = Color::new(0.75, 0.22, 0.17, 1.0); + let light_blue = Color::new(0.26, 0.62, 0.85, 1.0); - let event_appearance = container::Appearance { - background: Some(Background::Color(dark_grey)), - border_radius: 8.0.into(), - text_color: Some(Color::WHITE), - ..Default::default() - }; - - let message_appearance = container::Appearance { - background: Some(Background::Color(Color::WHITE)), - border_radius: 8.0.into(), - ..Default::default() - }; - - let own_message_appearance = container::Appearance { - background: Some(Background::Color(lighter_green)), - border_radius: 8.0.into(), - ..Default::default() - }; - - let messages = self - .message_log - .get(&self.active_channel) - .unwrap() - .iter() - .flat_map(|message| -> Option> { - match message { - IrcMessage::Join { nickname } => Some( - container( - container(text(format!("{nickname} joined the channel"))) - .style(move |_: &_| event_appearance) - .padding([3, 10]), - ) - .width(Length::Fill) - .center_x() - .padding([3, 0]) - .into(), - ), - IrcMessage::Part { nickname, reason } => { - let reason = match reason { - Some(reason) => format!(" ({reason})"), - None => String::new(), - }; - Some( - container( - container(text(format!("{nickname} left the channel{reason}"))) - .style(move |_: &_| event_appearance) - .padding([3, 10]), - ) - .width(Length::Fill) - .center_x() - .padding([3, 0]) - .into(), - ) - } - IrcMessage::Nick { old, new } => Some( - container( - container(text(format!("{old} changed their nickname to {new}"))) - .style(move |_: &_| event_appearance) - .padding([3, 10]), - ) - .width(Length::Fill) - .center_x() - .padding([3, 0]) - .into(), - ), - IrcMessage::Quit { nickname, reason } => { - let reason = match reason { - Some(reason) => format!(" ({reason})"), - None => String::new(), - }; - - Some( - container( - container(text(format!("{nickname} left the server{reason}"))) - .style(move |_: &_| event_appearance) - .padding([3, 10]), - ) - .width(Length::Fill) - .center_x() - .padding([3, 0]) - .into(), - ) - } - IrcMessage::Privmsg { nickname, message } => { - // TODO don't hardcode nickname lol - let is_self = nickname == "cri"; - - let mut elements = Vec::new(); - if !is_self { - elements.push(text(nickname).style(dark_red).into()) - } - elements.push(text(message).into()); - - Some( - container( - container(column(elements)) - .style(move |_: &_| { - if is_self { - own_message_appearance - } else { - message_appearance - } - }) - .padding([4, 10]), - ) - .width(Length::Fill) - .align_x(if is_self { - Horizontal::Right - } else { - Horizontal::Left - }) - .padding([4, 8]) - .into(), - ) - } - } - }) - .map(|element| element.into()) - .collect::>(); - - let log = container( - scrollable(column(messages)) - .height(Length::Fill) - .width(Length::Fill), - ) - .height(Length::Fill) - .width(Length::Fill) - .style(move |_: &_| container::Appearance { - background: Some(Background::Color(lighter_grey)), - ..Default::default() - }); + let log = self.message_log.view(&self.active_channel); let message_box = text_input("your magnum opus", &self.input_value) .id(INPUT_ID.clone()) @@ -339,34 +209,51 @@ impl Application for Cri { let channels = column( self.message_log - .get_channels() + .get_all() .iter() - .map(|channel| { - let text = text(channel); - let is_active = self.active_channel.as_ref() == Some(channel); - let container = container(text) - .style(move |_: &_| { - let background = if is_active { - Some(Background::Color(dark_grey)) - } else { - None - }; + .map(|(&ref channel, log)| { + let is_active = &self.active_channel == channel; + let channel_name = match channel { + None => "Server", + Some(channel) => channel, + }; - container::Appearance { - background, - ..Default::default() - } + let text_color = if is_active { Some(Color::WHITE) } else { None }; + let nickname_color = if is_active { Color::WHITE } else { light_blue }; + let no_message_color = if is_active { Color::WHITE } else { dark_grey }; + + let last_message = log + .iter() + .rev() + .find_map(|m| match m { + IrcMessage::Privmsg { nickname, message } => Some(row![ + text(format!("{nickname}: ")).style(nickname_color), + text(message) + ]), + _ => None, }) + .unwrap_or(row![text("No messages").style(no_message_color)]); + + let container = container(column![text(channel_name), last_message]) + .style(move |_: &_| container::Appearance { + background: match is_active { + true => Some(Background::Color(light_blue)), + false => None, + }, + text_color, + ..Default::default() + }) + .padding([4, 4]) .width(Length::Fill); mouse_area(container) - .on_press(UiMessage::HandleChannelPress(Some(channel.to_string()))) + .on_press(UiMessage::HandleChannelPress(channel.clone())) .into() }) .collect::>(), ) .height(Length::Fill) - .width(Length::Fixed(100.0)); + .width(Length::Fixed(200.0)); let content = row![channels, column![log, message_box].height(Length::Fill)]; diff --git a/src/message_log.rs b/src/message_log.rs index c611f61..56d3323 100644 --- a/src/message_log.rs +++ b/src/message_log.rs @@ -1,4 +1,10 @@ use crate::irc_message::IrcMessage; + +use iced::{ + alignment::Horizontal, + widget::{column, container, scrollable, text, Container}, + Background, Color, Length, +}; use std::collections::HashMap; pub struct MessageLog(HashMap, Vec>); @@ -10,10 +16,10 @@ impl<'a> MessageLog { Self(log) } - pub fn get_channels(&'a self) -> Vec<&String> { - let mut keys = self.0.keys().flatten().collect::>(); - keys.sort_unstable(); - keys + pub fn get_all(&'a self) -> Vec<(&'a Option, &'a Vec)> { + let mut log: Vec<(&Option, &Vec)> = self.0.iter().collect(); + log.sort_unstable_by_key(|(name, _)| name.as_deref()); + log } pub fn get(&self, channel: &Option) -> Option<&Vec> { @@ -61,4 +67,142 @@ impl<'a> MessageLog { message: message.to_string(), }) } + + pub fn view(&self, active_channel: &Option) -> Container<'_, crate::UiMessage> { + let lighter_grey = Color::new(0.93, 0.94, 0.95, 1.0); + let dark_grey = Color::new(0.58, 0.65, 0.65, 1.0); + let lighter_green = Color::new(0.94, 0.99, 0.87, 1.0); + let dark_red = Color::new(0.75, 0.22, 0.17, 1.0); + + let event_appearance = container::Appearance { + background: Some(Background::Color(dark_grey)), + border_radius: 8.0.into(), + text_color: Some(Color::WHITE), + ..Default::default() + }; + + let message_appearance = container::Appearance { + background: Some(Background::Color(Color::WHITE)), + border_radius: 8.0.into(), + ..Default::default() + }; + + let own_message_appearance = container::Appearance { + background: Some(Background::Color(lighter_green)), + border_radius: 8.0.into(), + ..Default::default() + }; + + let messages = self + .get(active_channel) + .unwrap() + .iter() + .flat_map(|message| -> Option> { + match message { + IrcMessage::Join { nickname } => Some( + container( + container(text(format!("{nickname} joined the channel"))) + .style(move |_: &_| event_appearance) + .padding([3, 10]), + ) + .width(Length::Fill) + .center_x() + .padding([3, 0]) + .into(), + ), + IrcMessage::Part { nickname, reason } => { + let reason = match reason { + Some(reason) => format!(" ({reason})"), + None => String::new(), + }; + Some( + container( + container(text(format!("{nickname} left the channel{reason}"))) + .style(move |_: &_| event_appearance) + .padding([3, 10]), + ) + .width(Length::Fill) + .center_x() + .padding([3, 0]) + .into(), + ) + } + IrcMessage::Nick { old, new } => Some( + container( + container(text(format!("{old} changed their nickname to {new}"))) + .style(move |_: &_| event_appearance) + .padding([3, 10]), + ) + .width(Length::Fill) + .center_x() + .padding([3, 0]) + .into(), + ), + IrcMessage::Quit { nickname, reason } => { + let reason = match reason { + Some(reason) => format!(" ({reason})"), + None => String::new(), + }; + + Some( + container( + container(text(format!("{nickname} left the server{reason}"))) + .style(move |_: &_| event_appearance) + .padding([3, 10]), + ) + .width(Length::Fill) + .center_x() + .padding([3, 0]) + .into(), + ) + } + IrcMessage::Privmsg { nickname, message } => { + // TODO don't hardcode nickname lol + let is_self = nickname == "cri"; + + let mut elements = Vec::new(); + if !is_self { + elements.push(text(nickname).style(dark_red).into()) + } + elements.push(text(message).into()); + + Some( + container( + container(column(elements)) + .style(move |_: &_| { + if is_self { + own_message_appearance + } else { + message_appearance + } + }) + .padding([4, 10]), + ) + .width(Length::Fill) + .align_x(if is_self { + Horizontal::Right + } else { + Horizontal::Left + }) + .padding([4, 8]) + .into(), + ) + } + } + }) + .map(|element| element.into()) + .collect::>(); + + container( + scrollable(column(messages)) + .height(Length::Fill) + .width(Length::Fill), + ) + .height(Length::Fill) + .width(Length::Fill) + .style(move |_: &_| container::Appearance { + background: Some(Background::Color(lighter_grey)), + ..Default::default() + }) + } }