From da99d04289489989fa349c5a924c00c5c3dcccfe Mon Sep 17 00:00:00 2001 From: GenericConfluent Date: Mon, 4 Mar 2024 21:08:41 -0700 Subject: [PATCH] feat: more ui scaffold --- src/lib.rs | 106 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 525f63b..73e0da4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ #![allow(dead_code, unused_variables)] +use std::collections::VecDeque; use std::sync::Arc; use iced::alignment::Horizontal; @@ -12,38 +13,85 @@ use iced_aw::{modal, split, Card}; mod course_database; use course_database::{CourseDatabase, CourseId}; use icons::Icon; +use petgraph::graph::{NodeIndex}; + +use anyhow::anyhow; mod graph_widget; mod icons; #[derive(Default)] pub struct FinescaleApp { + // This should be sorted. desired_courses: Vec, + required_courses: Option, course_database: Option, ui_states: UiStates, } #[derive(Default)] struct UiStates { - error_modal: Option, + error_modal: VecDeque, main_divider_pos: Option, course_input_val: String, } +/// UI events for the app. Do not clone `Message::LoadedCourses(...)`, and don't +/// put an Arc with a strong refcount != 1 in the variant. #[derive(Debug, Clone)] pub enum Message { LoadedCourses(Arc>), MainDividerResize(u16), CourseInputEvent(String), CourseInputSubmit, + CourseDelete(usize), IconsLoaded(Result<(), font::Error>), + // TODO: Invoke GraphViz as a subprocess and use the `open` crate to show its output. + ShowGraph, ClearError, } +// NOTE: May make sense to compress all the files into an archive and +// download from Github on each startup. async fn load_courses>(path: P) -> Arc> { CourseDatabase::new("[]").into() } +/// `desired` is guaranteed never to be empty and all the node indices are +/// valid. +fn select_courses(db: &CourseDatabase, desired: &[NodeIndex]) -> anyhow::Result { + Err(anyhow!("Course selection still needs implementation")) +} + +impl FinescaleApp { + fn update_required_courses(&mut self) -> anyhow::Result<()> { + if self.desired_courses.is_empty() { + self.course_database = None; + return Ok(()); + } + + let db = self + .course_database + .as_ref() + .ok_or(anyhow!("Course database is not yet loaded."))?; + let mut desired_courses = Vec::with_capacity(self.desired_courses.len()); + + for course_id in &self.desired_courses { + let Some(idx) = db.index_of(course_id) else { + self.ui_states.error_modal.push_back(format!( + "{} is not in course database. So its requirements will not be accounted for.", + course_id + )); + continue; + }; + desired_courses.push(idx); + } + + self.required_courses = Some(select_courses(db, &desired_courses)?); + Ok(()) + } +} + impl Application for FinescaleApp { type Executor = executor::Default; type Flags = (); @@ -66,19 +114,41 @@ impl Application for FinescaleApp { fn update(&mut self, _message: Self::Message) -> Command { match _message { - Message::ClearError => self.ui_states.error_modal = None, + // NOTE: The Arc is just being used to transport the data and allow Clone + // to be derived on Message. + Message::LoadedCourses(result_ptr) => match Arc::into_inner(result_ptr) { + Some(Ok(db)) => self.course_database = Some(db), + Some(Err(issue)) => self.ui_states.error_modal.push_back(issue.to_string()), + _ => panic!("Read the docs on Message"), + }, + Message::CourseDelete(idx) => { + _ = self.desired_courses.remove(idx); + _ = self + .update_required_courses() + .inspect_err(|e| self.ui_states.error_modal.push_back(e.to_string())); + } + Message::ClearError => _ = self.ui_states.error_modal.pop_front(), // TODO: Limit the divider movement Message::MainDividerResize(amt) => self.ui_states.main_divider_pos = Some(amt), Message::CourseInputEvent(val) => self.ui_states.course_input_val = val, Message::CourseInputSubmit => { match self.ui_states.course_input_val.parse::() { Ok(course) => { - self.desired_courses.push(course); + let Err(idx) = self.desired_courses.binary_search(&course) else { + self.ui_states + .error_modal + .push_back(format!("{} has already been added.", course)); + return Command::none(); + }; + + self.desired_courses.insert(idx, course); self.ui_states.course_input_val.clear(); + + _ = self + .update_required_courses() + .inspect_err(|e| self.ui_states.error_modal.push_back(e.to_string())); } - Err(issue) => { - self.ui_states.error_modal = Some(issue.to_string()); - } + Err(issue) => self.ui_states.error_modal.push_back(issue.to_string()), } } _ => {} @@ -102,19 +172,26 @@ impl Application for FinescaleApp { .spacing(10); let mut right = column![ - row![text("Required Classes") - .width(Length::Fill) - .size(40) - .style(Color::from_rgb(0.5, 0.5, 0.5)) - .horizontal_alignment(iced::alignment::Horizontal::Left),], + row![ + text("Required Classes") + .width(Length::Fill) + .size(40) + .style(Color::from_rgb(0.5, 0.5, 0.5)) + .horizontal_alignment(iced::alignment::Horizontal::Left), + button(Text::from(Icon::AccountTree)) + .padding(10) + .on_press(Message::ShowGraph) + ], horizontal_rule(2) ]; - for course in self.desired_courses.iter() { + for (idx, course) in self.desired_courses.iter().enumerate() { left = left.push( row![ text(course).width(Length::Fill), - button(Into::::into(Icon::DeleteForever)).padding(10) + button(Text::from(Icon::DeleteForever)) + .padding(10) + .on_press(Message::CourseDelete(idx)) ] .spacing(20) .align_items(iced::Alignment::Center), @@ -122,7 +199,6 @@ impl Application for FinescaleApp { right = right.push(text(course)); } - // Todo read and push courses. let main_content = Split::new( left, right, @@ -131,7 +207,7 @@ impl Application for FinescaleApp { Message::MainDividerResize, ); - let overlay = self.ui_states.error_modal.as_ref().map(|err_msg| { + let overlay = self.ui_states.error_modal.front().map(|err_msg| { Card::new(text("Error"), text(err_msg)) .foot( container(button("Ok").on_press(Message::ClearError))