From d5e2f67297db5e37e51eabf5a5456f1438623d0d Mon Sep 17 00:00:00 2001
From: Stephen D <webmaster@scd31.com>
Date: Sat, 11 May 2024 15:47:33 -0300
Subject: [PATCH] very basic chat

---
 Cargo.lock           |  18 +++++
 Cargo.toml           |   1 +
 src/controller.rs    |  21 +++++-
 src/gui/chat.rs      | 156 +++++++++++++++++++------------------------
 src/gui/chat_list.rs | 129 +++++++++++++++++++++++++++++++++++
 src/gui/mod.rs       |   1 +
 src/message.rs       |   8 +--
 7 files changed, 239 insertions(+), 95 deletions(-)
 create mode 100644 src/gui/chat_list.rs

diff --git a/Cargo.lock b/Cargo.lock
index a08924a..2d1a387 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -70,6 +70,7 @@ dependencies = [
  "cortex-m-rtic",
  "embedded-graphics",
  "embedded-hal 0.2.7",
+ "embedded-text",
  "fugit",
  "heapless 0.8.0",
  "panic-rtt-target",
@@ -216,6 +217,17 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89"
 
+[[package]]
+name = "embedded-text"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5899badc344754f785450bd64c04e03f6ceaacb5f3ae972fc1d93ae8473f4689"
+dependencies = [
+ "az",
+ "embedded-graphics",
+ "object-chain",
+]
+
 [[package]]
 name = "float-cmp"
 version = "0.9.0"
@@ -429,6 +441,12 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "object-chain"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41af26158b0f5530f7b79955006c2727cd23d0d8e7c3109dc316db0a919784dd"
+
 [[package]]
 name = "panic-rtt-target"
 version = "0.1.3"
diff --git a/Cargo.toml b/Cargo.toml
index 40fcb25..086be69 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,3 +20,4 @@ usbd-serial = "0.1"
 heapless = "0.8.0"
 rtt-target = "0.5.0"
 panic-rtt-target = "0.1.3"
+embedded-text = "0.7.1"
diff --git a/src/controller.rs b/src/controller.rs
index 091881e..1300f22 100644
--- a/src/controller.rs
+++ b/src/controller.rs
@@ -3,7 +3,10 @@ use heapless::{String, Vec};
 
 use crate::{
     contact::{Contact, ContactGroup, MAX_CONTACTS, MAX_CONTACT_NAME_LENGTH},
-    gui::{chat::Chat, contact_view::ContactView, selector::Selector, status::StatusBar, Element},
+    gui::{
+        chat::Chat, chat_list::ChatList, contact_view::ContactView, selector::Selector,
+        status::StatusBar, Element,
+    },
     keyboard::KeyCode,
     voltage::VoltMeter,
 };
@@ -19,6 +22,7 @@ pub enum View {
     ContactOptions(Selector<&'static str, [&'static str; 3]>, usize),
     #[allow(clippy::enum_variant_names)]
     ContactView(ContactView, usize),
+    ChatList(ChatList),
     Chat(Chat),
 }
 
@@ -46,7 +50,7 @@ impl View {
     pub fn from_main_menu_id(id: usize, contacts: &ContactGroup) -> Option<Self> {
         match id {
             0 => Some(Self::new_contacts(0, contacts)),
-            1 => Some(Self::Chat(Chat::new())),
+            1 => Some(Self::ChatList(ChatList::new())),
             _ => None,
         }
     }
@@ -54,7 +58,7 @@ impl View {
     pub fn to_main_menu_id(&self) -> usize {
         match self {
             View::Contacts(_) => 0,
-            View::Chat(_) => 1,
+            View::ChatList(_) => 1,
             _ => unreachable!(),
         }
     }
@@ -104,6 +108,7 @@ impl Element for View {
             View::Contacts(c) => c.render(dt),
             View::ContactOptions(c, _) => c.render(dt),
             View::ContactView(c, _) => c.render(dt),
+            View::ChatList(c) => c.render(dt),
             View::Chat(c) => c.render(dt),
         }
     }
@@ -116,6 +121,7 @@ impl Element for View {
             View::ContactView(c, _) => {
                 c.key_push(k);
             }
+            View::ChatList(c) => c.key_push(k),
             View::Chat(c) => c.key_push(k),
         }
     }
@@ -126,6 +132,7 @@ impl Element for View {
             View::Contacts(c) => c.touchpad_scroll(x, y),
             View::ContactOptions(c, _) => c.touchpad_scroll(x, y),
             View::ContactView(c, _) => c.touchpad_scroll(x, y),
+            View::ChatList(c) => c.touchpad_scroll(x, y),
             View::Chat(c) => c.touchpad_scroll(x, y),
         }
     }
@@ -194,6 +201,10 @@ impl Element for Controller {
                 }
             }
 
+            (View::ChatList(_), KeyCode::Touchpad) => {
+                self.cur_view = View::Chat(Chat::new());
+            }
+
             (View::MainMenu(_), KeyCode::Back) => {}
 
             (View::ContactOptions(_, id), KeyCode::Back) => {
@@ -205,6 +216,10 @@ impl Element for Controller {
                 self.cur_view = View::new_contact_options(*contact_id)
             }
 
+            (View::Chat(_), KeyCode::Back) => {
+                self.cur_view = View::ChatList(ChatList::new());
+            }
+
             (_, KeyCode::Back) => {
                 let id = self.cur_view.to_main_menu_id();
                 self.cur_view = View::new_main_menu(id);
diff --git a/src/gui/chat.rs b/src/gui/chat.rs
index 80e7afc..950bb19 100644
--- a/src/gui/chat.rs
+++ b/src/gui/chat.rs
@@ -1,44 +1,33 @@
-use core::fmt::Write;
-
 use embedded_graphics::{
-    mono_font::{ascii::FONT_10X20, MonoTextStyleBuilder},
-    pixelcolor::Rgb565,
-    prelude::{Point, RgbColor, Size},
+    geometry::{Dimensions, Point, Size},
+    mono_font::{ascii::FONT_10X20, MonoTextStyle},
+    pixelcolor::{Rgb565, RgbColor},
     primitives::{Primitive, PrimitiveStyleBuilder, Rectangle},
-    text::{Baseline, Text},
+    text::renderer::TextRenderer,
     Drawable,
 };
-use heapless::String;
-
-use crate::{app::WIDTH, message::MessageGroup};
+use embedded_text::{style::TextBoxStyleBuilder, TextBox};
 
-use super::{
-    scroll_tracker::{ScrollAction, ScrollTracker},
-    Element,
+use crate::{
+    app::WIDTH,
+    message::{MessageDirection, MessageGroup},
 };
 
-const STATUS_BAR_HEIGHT: i32 = 16;
-const GROUP_HEIGHT: i32 = 40;
-const BORDER_WIDTH: u32 = 4;
-const BACKGROUND_COLOR: Rgb565 = Rgb565::new(31, 41, 0); // #FFA500
-const BORDER_COLOR: Rgb565 = Rgb565::new(31, 26, 0); // 0xFF6600
+use super::{textbox::TextBox as MyTextBox, Element};
+
+const MESSAGE_BOX_WIDTH: u32 = 200;
+const MESSAGE_BOX_HEIGHT: u32 = 64;
 
 pub struct Chat {
-    groups: [MessageGroup; 3],
-    tracker: ScrollTracker,
-    selected: usize,
+    message_group: MessageGroup,
+    textbox: MyTextBox,
 }
 
 impl Chat {
     pub fn new() -> Self {
         Self {
-            groups: [
-                MessageGroup::default(),
-                MessageGroup::default(),
-                MessageGroup::default(),
-            ],
-            tracker: ScrollTracker::new(),
-            selected: 0,
+            message_group: MessageGroup::default(),
+            textbox: MyTextBox::new(1, 218, 31, 1),
         }
     }
 }
@@ -56,77 +45,68 @@ impl Element for Chat {
         &mut self,
         dt: &mut DT,
     ) -> Result<(), E> {
-        let unselected_rect_style = PrimitiveStyleBuilder::new()
-            .stroke_color(BORDER_COLOR)
-            .stroke_width(BORDER_WIDTH)
-            .fill_color(BACKGROUND_COLOR)
-            .build();
-
-        let selected_rect_style = PrimitiveStyleBuilder::new()
-            .stroke_color(BORDER_COLOR)
-            .stroke_width(BORDER_WIDTH)
+        let textbox_style = TextBoxStyleBuilder::new().build();
+        let character_style = MonoTextStyle::new(&FONT_10X20, Rgb565::BLACK);
+        let rect_style = PrimitiveStyleBuilder::new()
             .fill_color(Rgb565::WHITE)
             .build();
 
-        for (idx, group) in self.groups.iter().enumerate() {
-            let i = i32::try_from(idx).unwrap();
-
-            let x = 0;
-            let y =
-                i * (GROUP_HEIGHT + i32::try_from(BORDER_WIDTH).unwrap() * 2) + STATUS_BAR_HEIGHT;
-
-            // Draw outer rectangle
-            let rect_style = if idx == self.selected {
-                selected_rect_style
-            } else {
-                unselected_rect_style
-            };
-
-            Rectangle::new(
-                Point::new(0, y),
-                Size::new(
-                    u32::try_from(WIDTH).unwrap(),
-                    u32::try_from(GROUP_HEIGHT).unwrap(),
-                ),
-            )
-            .into_styled(rect_style)
-            .draw(dt)?;
-
-            // Draw text
-            let text_x = x + i32::try_from(BORDER_WIDTH).unwrap();
-            let text_y = y + i32::try_from(BORDER_WIDTH).unwrap();
-
-            let text_style = MonoTextStyleBuilder::new()
-                .font(&FONT_10X20)
-                .text_color(Rgb565::BLACK)
-                .build();
-
-            let mut text: String<25> = String::new();
-            write!(&mut text, "{}", group).unwrap();
-            Text::with_baseline(&text, Point::new(text_x, text_y), text_style, Baseline::Top)
-                .draw(dt)?;
+        let mut y = 28;
+        for msg in self.message_group.iter() {
+            let bounds = Rectangle::new(
+                Point::new(4, y),
+                Size::new(MESSAGE_BOX_WIDTH, MESSAGE_BOX_HEIGHT),
+            );
+
+            let mut text_box =
+                TextBox::with_textbox_style(msg.content, bounds, character_style, textbox_style);
+
+            let mut actual_bounds = calculate_bounding_box(&text_box);
+            if msg.direction == MessageDirection::To {
+                let x = i32::try_from(WIDTH).unwrap()
+                    - i32::try_from(actual_bounds.size.width).unwrap()
+                    - 4;
+
+                text_box.bounds.top_left.x = x;
+                actual_bounds.top_left.x = x;
+            }
+
+            actual_bounds.into_styled(rect_style).draw(dt)?;
+
+            text_box.draw(dt)?;
+
+            y += i32::try_from(actual_bounds.size.height).unwrap() + 4;
         }
 
+        self.textbox.render(dt)?;
+
         Ok(())
     }
 
-    fn key_push(&mut self, _: crate::keyboard::KeyCode) {
-        // TODO need to change states or something
+    fn key_push(&mut self, k: crate::keyboard::KeyCode) {
+        self.textbox.key_push(k);
     }
+}
 
-    fn touchpad_scroll(&mut self, _x: i8, y: i8) {
-        match self.tracker.scroll(y) {
-            ScrollAction::Previous => {
-                if self.selected > 0 {
-                    self.selected -= 1;
-                }
-            }
-            ScrollAction::Next => {
-                if self.selected < self.groups.len() - 1 {
-                    self.selected += 1;
-                }
-            }
-            ScrollAction::None => {}
+fn calculate_bounding_box<'a, S: TextRenderer>(tb: &TextBox<'a, S>) -> Rectangle {
+    const CHAR_WIDTH: u32 = 10;
+    const CHAR_HEIGHT: u32 = 20;
+
+    let mut x = 0;
+    let mut max_x = 0;
+    let mut y = 1;
+    for c in tb.text.chars() {
+        if c == '\n' || x >= CHAR_WIDTH * tb.bounds.size.width {
+            x = 0;
+            y += 1;
+        } else {
+            x += 1;
+            max_x = x;
         }
     }
+
+    Rectangle::new(
+        tb.bounds.top_left,
+        Size::new(max_x * CHAR_WIDTH, y * CHAR_HEIGHT),
+    )
 }
diff --git a/src/gui/chat_list.rs b/src/gui/chat_list.rs
new file mode 100644
index 0000000..de2e236
--- /dev/null
+++ b/src/gui/chat_list.rs
@@ -0,0 +1,129 @@
+use core::fmt::Write;
+
+use embedded_graphics::{
+    mono_font::{ascii::FONT_10X20, MonoTextStyleBuilder},
+    pixelcolor::Rgb565,
+    prelude::{Point, RgbColor, Size},
+    primitives::{Primitive, PrimitiveStyleBuilder, Rectangle},
+    text::{Baseline, Text},
+    Drawable,
+};
+use heapless::String;
+
+use crate::{app::WIDTH, message::MessageGroup};
+
+use super::{
+    scroll_tracker::{ScrollAction, ScrollTracker},
+    Element,
+};
+
+const STATUS_BAR_HEIGHT: i32 = 40;
+const GROUP_HEIGHT: i32 = 40;
+const BORDER_WIDTH: u32 = 4;
+const BACKGROUND_COLOR: Rgb565 = Rgb565::new(31, 41, 0); // #FFA500
+const BORDER_COLOR: Rgb565 = Rgb565::new(31, 26, 0); // #FF6600
+
+pub struct ChatList {
+    groups: [MessageGroup; 3],
+    tracker: ScrollTracker,
+    selected: usize,
+}
+
+impl ChatList {
+    pub fn new() -> Self {
+        Self {
+            groups: [
+                MessageGroup::default(),
+                MessageGroup::default(),
+                MessageGroup::default(),
+            ],
+            tracker: ScrollTracker::new(),
+            selected: 0,
+        }
+    }
+}
+
+impl Element for ChatList {
+    type KeyPushReturn = ();
+
+    fn render<
+        E,
+        DT: embedded_graphics::prelude::DrawTarget<
+            Color = embedded_graphics::pixelcolor::Rgb565,
+            Error = E,
+        >,
+    >(
+        &mut self,
+        dt: &mut DT,
+    ) -> Result<(), E> {
+        let unselected_rect_style = PrimitiveStyleBuilder::new()
+            .stroke_color(BORDER_COLOR)
+            .stroke_width(BORDER_WIDTH)
+            .fill_color(BACKGROUND_COLOR)
+            .build();
+
+        let selected_rect_style = PrimitiveStyleBuilder::new()
+            .stroke_color(BORDER_COLOR)
+            .stroke_width(BORDER_WIDTH)
+            .fill_color(Rgb565::WHITE)
+            .build();
+
+        for (idx, group) in self.groups.iter().enumerate() {
+            let i = i32::try_from(idx).unwrap();
+
+            let x = 0;
+            let y = i * (GROUP_HEIGHT + i32::try_from(BORDER_WIDTH).unwrap()) + STATUS_BAR_HEIGHT;
+
+            // Draw outer rectangle
+            let rect_style = if idx == self.selected {
+                selected_rect_style
+            } else {
+                unselected_rect_style
+            };
+
+            Rectangle::new(
+                Point::new(0, y),
+                Size::new(
+                    u32::try_from(WIDTH).unwrap(),
+                    u32::try_from(GROUP_HEIGHT).unwrap(),
+                ),
+            )
+            .into_styled(rect_style)
+            .draw(dt)?;
+
+            // Draw text
+            let text_x = x + i32::try_from(BORDER_WIDTH).unwrap();
+            let text_y = y + i32::try_from(BORDER_WIDTH).unwrap();
+
+            let text_style = MonoTextStyleBuilder::new()
+                .font(&FONT_10X20)
+                .text_color(Rgb565::BLACK)
+                .build();
+
+            let mut text: String<25> = String::new();
+            write!(&mut text, "{}", group).unwrap();
+            Text::with_baseline(&text, Point::new(text_x, text_y), text_style, Baseline::Top)
+                .draw(dt)?;
+        }
+
+        Ok(())
+    }
+
+    fn key_push(&mut self, _: crate::keyboard::KeyCode) {}
+
+    fn touchpad_scroll(&mut self, _x: i8, y: i8) {
+        match self.tracker.scroll(y) {
+            ScrollAction::Previous => {
+                if self.selected > 0 {
+                    self.selected -= 1;
+                }
+            }
+            ScrollAction::Next => {
+                if self.selected < self.groups.len() - 1 {
+                    self.selected += 1;
+                }
+            }
+            ScrollAction::None => {}
+        }
+    }
+}
diff --git a/src/gui/mod.rs b/src/gui/mod.rs
index f37fdbb..ec4a5b1 100644
--- a/src/gui/mod.rs
+++ b/src/gui/mod.rs
@@ -4,6 +4,7 @@ use crate::keyboard::KeyCode;
 
 pub mod button;
 pub mod chat;
+pub mod chat_list;
 pub mod contact_view;
 pub mod scroll_tracker;
 pub mod selector;
diff --git a/src/message.rs b/src/message.rs
index 058dc38..111ac81 100644
--- a/src/message.rs
+++ b/src/message.rs
@@ -1,6 +1,6 @@
 use core::fmt::Display;
 
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, PartialEq, Eq)]
 pub enum MessageDirection {
     From,
     To,
@@ -8,8 +8,8 @@ pub enum MessageDirection {
 
 #[derive(Copy, Clone)]
 pub struct Message {
-    content: &'static str,
-    direction: MessageDirection,
+    pub content: &'static str,
+    pub direction: MessageDirection,
 }
 
 // A single column in the message list
@@ -77,7 +77,7 @@ impl<'a> Iterator for MessageGroupIter<'a> {
             },
         ];
 
-        if self.i > stubbed.len() {
+        if self.i >= stubbed.len() {
             None
         } else {
             let out = stubbed[self.i];
-- 
GitLab