Skip to content

Commit

Permalink
feat: more ui scaffold
Browse files Browse the repository at this point in the history
  • Loading branch information
GenericConfluent committed Mar 5, 2024
1 parent 9f19f0e commit da99d04
Showing 1 changed file with 91 additions and 15 deletions.
106 changes: 91 additions & 15 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![allow(dead_code, unused_variables)]

use std::collections::VecDeque;
use std::sync::Arc;

use iced::alignment::Horizontal;
Expand All @@ -12,38 +13,85 @@ use iced_aw::{modal, split, Card};
mod course_database;

Check warning on line 13 in src/lib.rs

View workflow job for this annotation

GitHub Actions / fmt

Diff in /home/runner/work/finescale/finescale/src/lib.rs
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<CourseId>,
required_courses: Option<CourseDatabase>,
course_database: Option<CourseDatabase>,
ui_states: UiStates,
}

#[derive(Default)]
struct UiStates {
error_modal: Option<String>,
error_modal: VecDeque<String>,
main_divider_pos: Option<u16>,
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<anyhow::Result<CourseDatabase>>),
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<P: AsRef<std::path::Path>>(path: P) -> Arc<anyhow::Result<CourseDatabase>> {
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<CourseDatabase> {
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 = ();
Expand All @@ -66,19 +114,41 @@ impl Application for FinescaleApp {

fn update(&mut self, _message: Self::Message) -> Command<Self::Message> {
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::<CourseId>() {
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()),
}
}
_ => {}
Expand All @@ -102,27 +172,33 @@ 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::<Text>::into(Icon::DeleteForever)).padding(10)
button(Text::from(Icon::DeleteForever))
.padding(10)
.on_press(Message::CourseDelete(idx))
]
.spacing(20)
.align_items(iced::Alignment::Center),
);
right = right.push(text(course));
}

// Todo read and push courses.
let main_content = Split::new(
left,
right,
Expand All @@ -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))
Expand Down

0 comments on commit da99d04

Please sign in to comment.