diff --git a/Cargo.toml b/Cargo.toml index 7d3a00b..1e94b57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,6 @@ image = "0.25.2" rand = "0.8.5" rfd = "0.14.1" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +strum = { version = "0.26.3", features = ["derive"] } +emath = "0.28.1" +ecolor = "0.28.1" diff --git a/src/app.rs b/src/app.rs index d25d3c9..0538721 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,6 +3,9 @@ use egui::IconData; use libpt::cli::args::{VerbosityLevel, HELP_TEMPLATE}; use libpt::log::trace; +mod roll; +use roll::Roller; + pub const TITLE: &str = "Rollator"; /// Placeholder comment that will show in the help in the CLI @@ -19,6 +22,9 @@ pub struct RollatorApp { #[clap(skip)] value: f32, + #[clap(skip)] + roller: Roller, + #[serde(skip)] #[command(flatten)] pub(crate) verbosity: VerbosityLevel, @@ -36,6 +42,7 @@ impl Default for RollatorApp { value: 2.7, verbosity: VerbosityLevel::INFO, show_info_window: false, + roller: Roller::default(), } } } @@ -49,6 +56,7 @@ impl RollatorApp { self.label = old.label; self.value = old.value; + self.roller = old.roller; } } @@ -107,6 +115,54 @@ impl RollatorApp { }, ); } + + fn top_panel(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { + egui::menu::bar(ui, |ui| { + // NOTE: no File->Quit on web pages! + let is_web = cfg!(target_arch = "wasm32"); + if !is_web { + ui.menu_button("File", |ui| { + if ui.button("Info").clicked() { + self.show_info_window = true; + } + ui.separator(); + if ui.button("Quit").clicked() { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + }); + ui.add_space(16.0); + } + if self.show_info_window { + self.info_diag(ctx); + } + + egui::widgets::global_dark_light_mode_buttons(ui); + }); + } + + fn roller_panel(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { + ui.heading("Roll"); + + ui.vertical_centered_justified(|ui| { + self.roller.labels(ui); + self.roller.rolling_space(ui); + }); + } + + fn meta_panel(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { + // The central panel the region left after adding TopPanel's and SidePanel's + ui.heading(TITLE); + + ui.horizontal(|ui| { + ui.label("Write something: "); + ui.text_edit_singleline(&mut self.label); + }); + + ui.add(egui::Slider::new(&mut self.value, 0.0..=10.0).text("value")); + if ui.button("Increment").clicked() { + self.value += 1.0; + } + } } impl eframe::App for RollatorApp { @@ -117,71 +173,37 @@ impl eframe::App for RollatorApp { /// Called each time the UI needs repainting, which may be many times per second. fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - // Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`. - // For inspiration and more examples, go to https://emilk.github.io/egui - egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { - // The top panel is often a good place for a menu bar: - - egui::menu::bar(ui, |ui| { - // NOTE: no File->Quit on web pages! - let is_web = cfg!(target_arch = "wasm32"); - if !is_web { - ui.menu_button("File", |ui| { - if ui.button("Info").clicked() { - self.show_info_window = true; - } - ui.add_space(4.0); - if ui.button("Quit").clicked() { - ctx.send_viewport_cmd(egui::ViewportCommand::Close); - } - }); - ui.add_space(16.0); - } - if self.show_info_window { - self.info_diag(ctx); - } - - egui::widgets::global_dark_light_mode_buttons(ui); - }); + self.top_panel(ui, ctx); + }); + egui::TopBottomPanel::top("top_panel2").show(ctx, |ui| { + self.meta_panel(ui, ctx); }); - egui::CentralPanel::default().show(ctx, |ui| { - // The central panel the region left after adding TopPanel's and SidePanel's - ui.heading(TITLE); - - ui.horizontal(|ui| { - ui.label("Write something: "); - ui.text_edit_singleline(&mut self.label); - }); - - ui.add(egui::Slider::new(&mut self.value, 0.0..=10.0).text("value")); - if ui.button("Increment").clicked() { - self.value += 1.0; - } - - ui.separator(); - - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { - bottom_label(ui); - egui::warn_if_debug_build(ui); - }); + ui.heading("idk what to put here lol"); }); + egui::SidePanel::right(egui::Id::new("roll_panel")).show(ctx, |ui| { + self.roller_panel(ui, ctx); + }); + egui::TopBottomPanel::bottom("bot_panel").show(ctx, bottom_label); } } fn bottom_label(ui: &mut egui::Ui) { - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 0.0; - ui.label(format!("{TITLE} v{}", env!("CARGO_PKG_VERSION"))); - ui.label(" | Powered by "); - ui.hyperlink_to("egui", "https://github.com/emilk/egui"); - ui.label(" and "); - ui.hyperlink_to( - "eframe", - "https://github.com/emilk/egui/tree/master/crates/eframe", - ); - ui.label("."); + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label(format!("{TITLE} v{}", env!("CARGO_PKG_VERSION"))); + ui.label(" | Powered by "); + ui.hyperlink_to("egui", "https://github.com/emilk/egui"); + ui.label(" and "); + ui.hyperlink_to( + "eframe", + "https://github.com/emilk/egui/tree/master/crates/eframe", + ); + ui.label("."); + }); + egui::warn_if_debug_build(ui); }); } diff --git a/src/app/roll.rs b/src/app/roll.rs new file mode 100644 index 0000000..72456df --- /dev/null +++ b/src/app/roll.rs @@ -0,0 +1,150 @@ +use std::fmt::Display; + +use egui::{epaint::PathStroke, *}; +use egui::{vec2, Pos2, Rect}; +use libpt::log::{debug, trace}; +use rand::Rng; +use strum::{EnumIter, IntoEnumIterator}; + +#[derive( + serde::Deserialize, + serde::Serialize, + Debug, + Clone, + Default, + EnumIter, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + Copy, +)] +pub enum Dice { + D2, + D4, + D6, + D8, + D10, + D12, + #[default] + D20, + D100, +} + +impl From for u8 { + fn from(value: Dice) -> Self { + match value { + Dice::D2 => 2, + Dice::D4 => 4, + Dice::D6 => 6, + Dice::D8 => 8, + Dice::D10 => 10, + Dice::D12 => 12, + Dice::D20 => 20, + Dice::D100 => 100, + } + } +} + +impl Display for Dice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Dice::D2 => "D2", + Dice::D4 => "D4", + Dice::D6 => "D6", + Dice::D8 => "D8", + Dice::D10 => "D10", + Dice::D12 => "D12", + Dice::D20 => "D20", + Dice::D100 => "D100", + } + ) + } +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, Default)] +pub struct Roller { + selected_dice: Dice, + new: bool, + value: u8, +} + +impl Roller { + pub(crate) fn roll(&mut self) -> u8 { + if self.new { + self.value = rand::thread_rng().gen_range(1..=self.selected_dice.into()); + self.new = false; + } + self.value + } + + pub(crate) fn labels(&mut self, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + for variant in Dice::iter() { + let dice_label = + ui.selectable_label(variant == self.selected_dice, variant.to_string()); + if dice_label.clicked() { + self.selected_dice = variant; + } + } + }); + ui.separator(); + } + + pub(crate) fn rolling_space(&mut self, ui: &mut egui::Ui) { + let color = if ui.visuals().dark_mode { + Color32::from_additive_luminance(196) + } else { + Color32::from_black_alpha(240) + }; + + let button = ui.button("> ROLL <"); + if button.clicked() { + debug!("rolling dice"); + self.new = true; + } + ui.add_space(10.0); + ui.heading(self.roll().to_string()); + ui.add_space(10.0); + ui.centered_and_justified(|ui| { + let _canvas = egui::containers::Frame::canvas(ui.style()).show(ui, |ui| { + ui.ctx().request_repaint(); + let time = ui.input(|i| i.time); + + let desired_size = ui.available_width() * vec2(1.0, 0.35); + let (_id, rect) = ui.allocate_space(desired_size); + + let to_screen = emath::RectTransform::from_to( + Rect::from_x_y_ranges(0.0..=1.0, -1.0..=1.0), + rect, + ); + let mut shapes = vec![]; + + for &mode in &[2.0, 3.0, 5.0] { + let n = 120; + let speed = 1.5; + + let points: Vec = (0..=n) + .map(|i| { + let t = i as f64 / (n as f64); + let amp = (time * speed * mode).sin() / mode; + let y = amp * (t * std::f64::consts::TAU / 2.0 * mode).sin(); + to_screen * pos2(t as f32, y as f32) + }) + .collect(); + + let thickness = 10.0 / mode as f32; + shapes.push(epaint::Shape::line( + points, + PathStroke::new(thickness, color), + )); + } + ui.painter().extend(shapes); + }); + }); + } +} diff --git a/src/lib.rs b/src/lib.rs index 6ae8080..3634415 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ #![warn(clippy::all, rust_2018_idioms)] -mod app; +pub(crate) mod app; pub use app::RollatorApp; diff --git a/src/main.rs b/src/main.rs index 833f53e..96615f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,26 +6,28 @@ mod app; // When compiling natively: #[cfg(not(target_arch = "wasm32"))] fn main() -> eframe::Result { - use libpt::log::debug; + use libpt::log::{debug, error}; use tracing_subscriber::util::SubscriberInitExt; let mut app = app::RollatorApp::new_with_cli(); let filter = tracing_subscriber::EnvFilter::builder() - .with_default_directive(tracing_subscriber::filter::LevelFilter::WARN.into()) + .with_default_directive(app.verbosity.level().into()) .from_env() .expect("could not init logger") .add_directive( - format!("rollator={}", app.verbosity.level()) + format!("{}={}", env!("CARGO_PKG_NAME"), app.verbosity.level()) .parse() .expect("could not init logger"), ); tracing_subscriber::fmt::Subscriber::builder() .with_env_filter(filter) + .with_max_level(app.verbosity.level()) .finish() - .set_default(); + .init(); debug!("logging initialized!"); + debug!("level: {}", app.verbosity.level()); let native_options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default()