feat(roll): a basic roll with a few sines to show entropy or something
cargo devel CI / cargo CI (push) Successful in 3m4s Details

This commit is contained in:
Christoph J. Scherr 2024-08-13 20:49:40 +02:00
parent 5f541f4009
commit 62ee0ee0ab
5 changed files with 239 additions and 62 deletions

View File

@ -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"

View File

@ -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);
});
}

150
src/app/roll.rs Normal file
View File

@ -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<Dice> 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<Pos2> = (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);
});
});
}
}

View File

@ -1,4 +1,4 @@
#![warn(clippy::all, rust_2018_idioms)]
mod app;
pub(crate) mod app;
pub use app::RollatorApp;

View File

@ -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()