From 6505d219de6cd53301a001a215bcdabbcf8e20f0 Mon Sep 17 00:00:00 2001 From: FantasyPvP <80643031+FantasyPvP@users.noreply.github.com> Date: Thu, 21 Mar 2024 00:12:53 +0000 Subject: [PATCH] - Created Dialog widget - standard information dialog works TODO: implement more complex dialogs where multiple options can be selected --- src/lib.rs | 1 + src/system/kernel/render.rs | 1 + src/system/kernel/tasks/keyboard.rs | 2 + src/system/std/os.rs | 2 +- src/user/bin/crystalfetch.rs | 4 +- src/user/bin/shell.rs | 30 ++--- src/user/lib/libgui/cg_utils.rs | 2 +- src/user/lib/libgui/cg_widgets.rs | 166 ++++++++++++++++++++++++---- 8 files changed, 171 insertions(+), 37 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f1e7cc2..c79743e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ #![feature(async_fn_in_trait)] #![feature(async_closure)] #![feature(global_asm)] +#![feature(inherent_associated_types)] use alloc::string::String; diff --git a/src/system/kernel/render.rs b/src/system/kernel/render.rs index 083f5c7..56c0e58 100644 --- a/src/system/kernel/render.rs +++ b/src/system/kernel/render.rs @@ -52,6 +52,7 @@ pub struct ScreenChar { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RenderError { OutOfBounds(bool, bool), // (bool, bool) refers to x and y respectively + TooSmall, InvalidCharacter, InvalidColour, InvalidRenderMode, diff --git a/src/system/kernel/tasks/keyboard.rs b/src/system/kernel/tasks/keyboard.rs index 4f1be63..9d44b49 100644 --- a/src/system/kernel/tasks/keyboard.rs +++ b/src/system/kernel/tasks/keyboard.rs @@ -49,6 +49,7 @@ pub enum KeyStroke { Left, Right, None, + Enter } impl KeyStroke { @@ -65,6 +66,7 @@ impl KeyStroke { KeyCode::Backspace => KeyStroke::Backspace, KeyCode::ArrowLeft => KeyStroke::Left, KeyCode::ArrowRight => KeyStroke::Right, + KeyCode::Enter => KeyStroke::Enter, _ => KeyStroke::None, } } diff --git a/src/system/std/os.rs b/src/system/std/os.rs index dba84c0..8e3bd37 100644 --- a/src/system/std/os.rs +++ b/src/system/std/os.rs @@ -5,7 +5,7 @@ use alloc::{string::String}; lazy_static! { pub static ref OS: Mutex = Mutex::new(SysInfo { os: String::from("CrystalOS Alpha"), - version: String::from("0.2.1"), + version: String::from("0.2.2"), }); } diff --git a/src/user/bin/crystalfetch.rs b/src/user/bin/crystalfetch.rs index 5e24366..3df08b4 100644 --- a/src/user/bin/crystalfetch.rs +++ b/src/user/bin/crystalfetch.rs @@ -55,8 +55,8 @@ impl Application for CrystalFetch { " [ OS » {} [ BUILD » {} [ Shell » CrySH - [ Github » https://github.com/FantasyPvP/CrystalOS-Restructured - [ Author » FantasyPvP / ZXQ5", os, version); + [ Github » https://github.com/FantasyPvP/CrystalOS + [ Author » ZXQ5", os, version); // write to output let spacer = "\n".repeat(25 - logo_string.lines().count() - 4 - info_string.lines().count()); diff --git a/src/user/bin/shell.rs b/src/user/bin/shell.rs index f916302..3904dba 100644 --- a/src/user/bin/shell.rs +++ b/src/user/bin/shell.rs @@ -10,6 +10,7 @@ use crate::{print, printerr, println, serial_println, std, std::application::{Ap use crate::std::frame::{Dimensions, Position, ColorCode}; use crate::std::io::{Color, write, Screen, Stdin, Serial, KeyStroke}; use crate::std::random::Random; +use crate::std::time::timer; use crate::user::bin::gigachad_detector::GigachadDetector; use crate::user::bin::grapher::Grapher; use crate::user::lib::libgui::{ @@ -17,8 +18,8 @@ use crate::user::lib::libgui::{ cg_widgets::{CgTextBox, CgContainer, CgIndicatorBar, CgIndicatorWidget, CgLabel, CgStatusBar}, cg_inputs::CgLineEdit, }; -use crate::user::lib::libgui::cg_core::{CgTextInput, Widget}; -use crate::user::lib::libgui::cg_widgets::CgDialog; +use crate::user::lib::libgui::cg_core::{CgKeyboardCapture, CgTextInput, Widget}; +use crate::user::lib::libgui::cg_widgets::{CgDialog, CgDialogType}; lazy_static! { pub static ref CMD: Mutex = Mutex::new(CommandHandler::new()); @@ -254,17 +255,23 @@ struct CmdHistory { } async fn setup_ui() { - let dialog = CgDialog::new( - Dimensions::new(40, 10), - String::from("test dialog"), - String::from("dialog body"), - String::from("[dialog footer]") + let exit = |x: KeyStroke| { match x { + KeyStroke::Char('`') => (KeyStroke::None, Exit::Exit), + _ => (x, Exit::None), + }}; + + let mut dialog = CgDialog::new( + String::from("i'd just like to interject for a moment"), + String::from("The kernel is an essential part of an operating system, but useless by itself; it can only function in the context of a complete operating system. Linux is normally used in combination with the GNU operating system: the whole system is basically GNU with Linux added, or GNU/Linux. All the so-called Linux distributions are really distributions of GNU/Linux!"), + // CgDialog::Type::Selection(vec![String::from("Shut Up Nerd"), String::from("Ok Boomer"), String::from("Nice")]), + CgDialogType::Information ); if let Ok(frame) = dialog.render() { frame.write_to_screen().unwrap(); } - return; + dialog.keyboard_capture(exit, None).await.unwrap(); + serial_println!("idk"); let label= Widget::insert(CgLabel::new( @@ -276,7 +283,7 @@ async fn setup_ui() { let textbox = Widget::insert(CgTextBox::new( String::from("i'd just like to interject for a moment"), - String::from("I'd just like to interject for a moment. What you're refering to as Linux, is in fact, GNU/Linux, or as I've recently taken to calling it, GNU plus Linux. Linux is not an operating system unto itself, but rather another free component of a fully functioning GNU system made useful by the GNU corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX. Many computer users run a modified version of the GNU system every day, without realizing it. Through a peculiar turn of events, the version of GNU which is widely used today is often called Linux, and many of its users are not aware that it is basically the GNU system, developed by the GNU Project. There really is a Linux, and these people are using it, but it is just a part of the system they use. Linux is the kernel: the program in the system that allocates the machine's resources to the other programs that you run. The kernel is an essential part of an operating system, but useless by itself; it can only function in the context of a complete operating system. Linux is normally used in combination with the GNU operating system: the whole system is basically GNU with Linux added, or GNU/Linux. All the so-called Linux distributions are really distributions of GNU/Linux!"), + String::from("I'd just like to interject for a moment. What you're referring to as Linux, is in fact, GNU/Linux, or as I've recently taken to calling it, GNU plus Linux. Linux is not an operating system unto itself, but rather another free component of a fully functioning GNU system made useful by the GNU corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX. Many computer users run a modified version of the GNU system every day, without realizing it. Through a peculiar turn of events, the version of GNU which is widely used today is often called Linux, and many of its users are not aware that it is basically the GNU system, developed by the GNU Project. There really is a Linux, and these people are using it, but it is just a part of the system they use. Linux is the kernel: the program in the system that allocates the machine's resources to the other programs that you run. The kernel is an essential part of an operating system, but useless by itself; it can only function in the context of a complete operating system. Linux is normally used in combination with the GNU operating system: the whole system is basically GNU with Linux added, or GNU/Linux. All the so-called Linux distributions are really distributions of GNU/Linux!"), Position::new(2, 5), Dimensions::new(40, 12), true, @@ -309,10 +316,7 @@ async fn setup_ui() { } - let exit = |x: KeyStroke| { match x { - KeyStroke::Char('`') => (KeyStroke::None, Exit::Exit), - _ => (x, Exit::None), - }}; + let container_copy = container.fetch::().unwrap(); let entry_ref = container_copy.fetch("textedit").unwrap(); diff --git a/src/user/lib/libgui/cg_utils.rs b/src/user/lib/libgui/cg_utils.rs index 948f479..88ebeb5 100644 --- a/src/user/lib/libgui/cg_utils.rs +++ b/src/user/lib/libgui/cg_utils.rs @@ -18,4 +18,4 @@ pub(crate) fn render_outline(frame: &mut Frame, dimensions: Dimensions) { frame.write(Position::new(dimensions.x - 1, 0), ColouredChar::new('┐')); frame.write(Position::new(0, dimensions.y - 1), ColouredChar::new('└')); frame.write(Position::new(dimensions.x - 1, dimensions.y - 1), ColouredChar::new('┘')); -} \ No newline at end of file +} diff --git a/src/user/lib/libgui/cg_widgets.rs b/src/user/lib/libgui/cg_widgets.rs index a897350..1d478c6 100644 --- a/src/user/lib/libgui/cg_widgets.rs +++ b/src/user/lib/libgui/cg_widgets.rs @@ -3,12 +3,14 @@ use alloc::fmt::format; use alloc::string::ToString; use core::any::Any; use core::cmp::{max, min}; +use async_trait::async_trait; use hashbrown::HashMap; use crate::serial_println; -use super::cg_core::{CgComponent, CgOutline, Widget}; +use crate::std::application::Exit; +use super::cg_core::{CgComponent, CgKeyboardCapture, CgOutline, Widget}; use super::cg_utils::render_outline; use crate::std::frame::{ColouredChar, Dimensions, Position, Frame, RenderError, ColorCode, BUFFER_WIDTH, BUFFER_HEIGHT}; -use crate::std::io::Color; +use crate::std::io::{Color, KeyStroke, Stdin}; #[derive(Debug, Clone)] pub struct CgContainer { @@ -74,7 +76,7 @@ impl CgTextBox { fn render_title(&self, frame: &mut Frame) { let title = self.title.chars(); for (i, c) in title.enumerate() { - if i + 2 == self.dimensions.x - 3 { // we dont want to write at the top of the text box + if i + 2 == self.dimensions.x - 3 { // we don't want to write at the top of the text box frame.write(Position::new(i + 1, 0), ColouredChar::new('.')); } else if i + 2 >= self.dimensions.x - 2 { frame.write(Position::new(i + 1, 0), ColouredChar::new('.')); @@ -102,7 +104,7 @@ impl CgComponent for CgTextBox { for word in self.content.split(' ') { if self.wrap_words { - if word.len() > self.dimensions.x - 2 - x { + if word.len() + 1 > 1 + self.dimensions.x - 2 - x { if word.len() <= self.dimensions.x - 2 { x = 1; y += 1; @@ -345,46 +347,150 @@ impl CgStatusBar { } } -enum CgDialogType { +pub enum CgDialogType { Information, Confirmation, + Selection(Vec), } pub struct CgDialog { - dimensions: Dimensions, + width: usize, title: String, content: String, - button_text: String, accepted: bool, outlined: bool, + dialog_class: CgDialogType, } impl CgDialog { - pub(crate) fn new(dimensions: Dimensions, title: String, content: String, button_text: String) -> CgDialog { + pub(crate) fn new(title: String, content: String, class: CgDialogType) -> CgDialog { CgDialog { - dimensions, + width: 40, title, content, - button_text, accepted: false, outlined: true, + dialog_class: class, } } } + +// TODO: make dialogs responsive. impl CgComponent for CgDialog { fn render(&self) -> Result { - if self.dimensions.x > BUFFER_WIDTH || self.dimensions.y > BUFFER_HEIGHT { - return Err(RenderError::OutOfBounds(self.dimensions.x > BUFFER_WIDTH, self.dimensions.y > BUFFER_HEIGHT)); - } - let x_offset = (BUFFER_WIDTH - self.dimensions.x) / 2; - let y_offset = (BUFFER_HEIGHT - self.dimensions.y) / 2; - let mut frame = Frame::new(Position::new(x_offset, y_offset), Dimensions::new(self.dimensions.x, self.dimensions.y))?; + // find the size needed for the dialog buttons + let dialog_button_width = match &self.dialog_class { + CgDialogType::Selection(options) => { + options.iter().fold(0, |sum, x| sum + 5 + x.len()) // [ Option ] for each option + }, + CgDialogType::Information => 6, // [ Ok ] + CgDialogType::Confirmation => 22 // [ Confirm ] [ Cancel ] + }; - if self.outlined { - render_outline(&mut frame, self.dimensions); + // picks the largest out of the title length, dialog button length and 40 to determine the minimum width of the dialog. + let mut width = max(max(self.title.len(), dialog_button_width), 40); + + // we set the base height to 5, assuming the content is none. + let mut height = 5; + + // calculate required width and height of textbox based on the size of the content. + while self.content.len() as f32 * 1.25 / width as f32 >= BUFFER_HEIGHT as f32 - 8.0 + 1.0 { // the + 1.0 accounts for decimal values being truncated down, ensuring that the max height of 25 can be reached. + if width < BUFFER_WIDTH - 4 { + width += 1; + } else { + // in the case that the text does not fit within the dialog + // TODO: handle this properly + return Err(RenderError::OutOfBounds(true, true)); + } + }; + height = (self.content.len() as f32 * 1.25 / (width as f32)) as usize; + + // account for borders + width += 4; + height += 8; + + // offsets to centre the dialog + let x_offset = (BUFFER_WIDTH - width) / 2; + let y_offset = (BUFFER_HEIGHT - height) / 2; + + // now that we know the X and Y offsets, we can start to draw the frame + let mut frame = Frame::new(Position::new(x_offset, y_offset), Dimensions::new(width, height))?; + render_outline(&mut frame, Dimensions::new(width, height)); + + + // render title + let title_offset = (width - self.title.len()) / 2; + let title = CgLabel::new(self.title.clone(), Position::new(title_offset, 2), self.title.len(), true); + frame.place_child_element(&title.render().unwrap()); + + + let (mut x, mut y) = (2, 5); // top left of the text box + for word in self.content.split(' ') { + if word.len() + 1 > 1 + width - 4 - x { // adding a +1 on both sides accounts for the possible negative value at the end of the line, avoiding integer underflow. + if word.len() <= width - 4 { + x = 2; + y += 1; + } + } + + for c in format!("{} ", word).chars() { + if x >= width - 3 { + x = 2; + y += 1; + if c == ' ' { + continue; + } + } + if y >= height - 4 { + break; + } + + frame.write(Position::new(x, y), ColouredChar::new(c)); + x += 1; + }; } + + // dialog buttons + match &self.dialog_class { + CgDialogType::Information => { + let button_x_offset = (width - 6) / 2; + "[ Ok ]".chars().enumerate().for_each(|(i, c)| { + frame.write(Position::new(button_x_offset + i, height - 3), ColouredChar { + character: c, + colour: ColorCode::new(Color::Black, Color::White), + }); + }) + } + CgDialogType::Confirmation => { + let button_x_offset = (width - 22) / 2; + let button_y_offset = height - 3; + "[ Confirm ]".chars().enumerate().for_each(|(i, c)| { + frame.write(Position::new(button_x_offset + i, button_y_offset), ColouredChar { + character: c, + colour: ColorCode::new(Color::Black, Color::White), + }); + }); + "[ Cancel ]".chars().enumerate().for_each(|(i, c)| { + frame.write(Position::new(button_x_offset + i, button_y_offset + 1), ColouredChar { + character: c, + colour: ColorCode::new(Color::Black, Color::White), + }); + }); + }, + CgDialogType::Selection(options) => { + let button_string = options.iter().map(|option| format!("[ {} ] ", option)).collect::(); + let button_x_offset = (width - button_string.len()) / 2; + button_string.chars().enumerate().for_each(|(i, c)| { + frame.write(Position::new(button_x_offset + i, height - 3), ColouredChar { + character: c, + colour: ColorCode::new(Color::Black, Color::White), + }); + }) + } + }; + Ok(frame) } @@ -394,8 +500,28 @@ impl CgComponent for CgDialog { } } - - +#[async_trait] +impl CgKeyboardCapture for CgDialog { + async fn keyboard_capture(&mut self, break_condition: fn(KeyStroke) -> (KeyStroke, Exit), app: Option<&Widget>) -> Result { + loop { + let k = break_condition(Stdin::keystroke().await); + serial_println!("captured: {:?}", k.0); + match k { + (KeyStroke::Char('\n'), _) => { + return Ok(true) + }, + _ => {} + } + } + } +} + +impl CgDialog { + pub type Type = CgDialogType; + + fn dynamic_layout(&mut self) { + } +}