diff --git a/src/desktop.rs b/src/desktop.rs deleted file mode 100644 index aebac08..0000000 --- a/src/desktop.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::{ - sync::mpsc::{channel, Sender}, - time::Instant, -}; - -use eframe::{ - egui::{self, Direction, Layout, RichText, Ui}, - epaint::FontId, -}; -use egui_extras::{RetainedImage, Size, StripBuilder}; - -use crate::{mk_qr_bytes, Content, Flasher, StreamStatus}; - -const TEXT_FACTOR: f32 = 0.05; -const IMG_AREA_PCT: f32 = 0.85; - -impl eframe::App for Flasher { - fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - ctx.request_repaint_after(self.sleep); - let height = ctx.screen_rect().height(); - let tsize = (height * TEXT_FACTOR) - 10.0; - let streaming_height = (height * IMG_AREA_PCT) - 50.0; - let static_height = height - 50.0; - egui::CentralPanel::default().show(ctx, |ui| { - ui.heading(&self.heading); - match self.content { - Content::Static(bytes) => { - let img = RetainedImage::from_image_bytes( - "generated qr code", - &mk_qr_bytes(bytes, static_height), - ) - .unwrap(); - img.show(ui); - } - Content::Streamed(ref mut streaming_content) => match streaming_content.status { - StreamStatus::Paused => { - let img = RetainedImage::from_image_bytes( - "tx config for receiver initialization", - &mk_qr_bytes(streaming_content.txconfig, streaming_height), - ) - .unwrap(); - let (tx, rx) = channel(); - let button = ( - "Scan, then click to begin streaming", - StreamStatus::Streaming, - ); - - streaming_ui(ui, button, tx, img, tsize); - if let Ok(new_status) = rx.try_recv() { - streaming_content.status = new_status; - } - self.last = Instant::now(); - } - StreamStatus::Streaming => { - let dur = Instant::now() - self.last; - let img = if dur < self.sleep { - if let Some(bytes) = streaming_content.last_packet.clone() { - RetainedImage::from_image_bytes( - "last packet", - &mk_qr_bytes(&bytes, streaming_height), - ) - .unwrap() - } else { - return; - } - } else { - let bytes = streaming_content.rx.recv().unwrap(); - self.last = Instant::now(); - streaming_content.last_packet = Some(bytes.clone()); - RetainedImage::from_image_bytes( - "new packet", - &mk_qr_bytes(&bytes, streaming_height), - ) - .unwrap() - }; - - let (tx, rx) = channel(); - let button = ("pause and show txconfig", StreamStatus::Paused); - streaming_ui(ui, button, tx, img, tsize); - if let Ok(new_status) = rx.try_recv() { - streaming_content.status = new_status; - } - } - }, - }; - // check for quit key - if ui.input(|i| i.key_pressed(egui::Key::Q)) - || ui.input(|i| i.key_pressed(egui::Key::Escape)) - { - frame.close(); - } - }); - } -} - -fn streaming_ui( - ui: &mut Ui, - button: (&str, StreamStatus), - sender: Sender, - image: RetainedImage, - tsize: f32, -) { - StripBuilder::new(ui) - .size(Size::relative(0.10)) - .size(Size::remainder()) - .cell_layout(Layout::centered_and_justified(Direction::TopDown)) - .vertical(|mut strip| { - strip.strip(|pstrip| { - pstrip.sizes(Size::remainder(), 1).horizontal(|mut pstrip| { - pstrip.cell(|ui| { - let button_text = RichText::new(button.0).font(FontId::monospace(tsize)); - if ui.button(button_text.clone()).clicked() { - sender.send(button.1).unwrap(); - } - }); - }); - }); - strip.cell(|ui| { - image.show(ui); - }); - }); -} diff --git a/src/desktop/mod.rs b/src/desktop/mod.rs new file mode 100644 index 0000000..1dd8db7 --- /dev/null +++ b/src/desktop/mod.rs @@ -0,0 +1,133 @@ +use std::{ + sync::mpsc::{channel, Sender}, + time::{Duration, Instant}, +}; + +use eframe::{ + egui::{self, Direction, Layout, RichText, Ui}, + epaint::FontId, +}; +use egui_extras::{RetainedImage, Size, StripBuilder}; + +use crate::{mk_qr_bytes, Content, Flasher, StreamStatus, StreamedContent}; + +const TEXT_FACTOR: f32 = 0.05; +const IMG_AREA_PCT: f32 = 0.85; + +struct Button<'text> { + pub text: &'text str, + pub next_state: StreamStatus, +} + +impl eframe::App for Flasher { + fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + ctx.request_repaint_after(self.sleep); + let height = ctx.screen_rect().height(); + let tsize = (height * TEXT_FACTOR) - 10.0; + let streaming_height = (height * IMG_AREA_PCT) - 50.0; + let static_height = height - 50.0; + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading(&self.description); + match self.content { + Content::Static(bytes) => { + let img = RetainedImage::from_image_bytes( + "static content", + &mk_qr_bytes(bytes, static_height), + ) + .unwrap(); + + // If it's static, just show the thing, don't set up any other UI or buttons or + // anything + img.show(ui); + } + Content::Streamed(ref mut streamed_content) => match streamed_content.status { + StreamStatus::Paused => { + paused(streamed_content, ui, streaming_height, tsize); + } + StreamStatus::Streaming => { + streaming(streamed_content, ui, streaming_height, tsize, self.sleep); + } + }, + } + // check for quit key + if ui.input(|i| i.key_pressed(egui::Key::Q)) + || ui.input(|i| i.key_pressed(egui::Key::Escape)) + { + frame.close(); + } + }); + } +} + +fn paused(sc: &mut StreamedContent, ui: &mut Ui, height: f32, tsize: f32) { + let img = RetainedImage::from_image_bytes( + "tx config for receiver initialization", + &mk_qr_bytes(sc.txconfig, height), + ) + .unwrap(); + let (tx, rx) = channel(); + let button = Button { + text: "Scan, then click to begin streaming", + next_state: StreamStatus::Streaming, + }; + + render_streaming_ui(ui, button, tx, img, tsize); + if let Ok(new_status) = rx.try_recv() { + sc.status = new_status; + } + sc.last_packet_time = Instant::now(); +} + +fn streaming(sc: &mut StreamedContent, ui: &mut Ui, height: f32, tsize: f32, sleep: Duration) { + let dur = Instant::now() - sc.last_packet_time; + let img = if dur < sleep { + if let Some(bytes) = sc.last_packet.clone() { + RetainedImage::from_image_bytes("last packet", &mk_qr_bytes(&bytes, height)).unwrap() + } else { + return; + } + } else { + let bytes = sc.rx.recv().unwrap(); + sc.last_packet = Some(bytes.clone()); + sc.last_packet_time = Instant::now(); + RetainedImage::from_image_bytes("new packet", &mk_qr_bytes(&bytes, height)).unwrap() + }; + + let (tx, rx) = channel(); + let button = Button { + text: "pause and show txconfig", + next_state: StreamStatus::Paused, + }; + render_streaming_ui(ui, button, tx, img, tsize); + if let Ok(new_status) = rx.try_recv() { + sc.status = new_status; + } +} + +fn render_streaming_ui( + ui: &mut Ui, + button: Button, + sender: Sender, + image: RetainedImage, + tsize: f32, +) { + StripBuilder::new(ui) + .size(Size::relative(0.10)) + .size(Size::remainder()) + .cell_layout(Layout::centered_and_justified(Direction::TopDown)) + .vertical(|mut strip| { + strip.strip(|pstrip| { + pstrip.sizes(Size::remainder(), 1).horizontal(|mut pstrip| { + pstrip.cell(|ui| { + let button_text = RichText::new(button.text).font(FontId::monospace(tsize)); + if ui.button(button_text.clone()).clicked() { + sender.send(button.next_state).unwrap(); + } + }); + }); + }); + strip.cell(|ui| { + image.show(ui); + }); + }); +} diff --git a/src/lib.rs b/src/lib.rs index a1af47e..41c0190 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ use rkyv::{Archive, Deserialize, Serialize}; mod desktop; mod util; -pub use util::{mk_qr_bytes, stream_bytes}; +pub use util::{get_content, mk_qr_bytes, stream_bytes}; pub type CuttleSender = std::sync::mpsc::SyncSender>; pub type CuttleReceiver = std::sync::mpsc::Receiver>; @@ -17,10 +17,21 @@ pub type CuttleReceiver = std::sync::mpsc::Receiver>; /// The application state #[derive(Debug)] pub struct Flasher { - pub heading: String, + pub description: String, pub content: Content, pub sleep: Duration, - pub last: Instant, +} + +impl Flasher { + pub fn new(description: String, content: Content, fps: f64) -> Self { + let sleep = 1000.0 / fps; + let sleep = Duration::from_millis(sleep as u64); + Flasher { + description, + content, + sleep, + } + } } #[derive(Debug)] @@ -35,6 +46,19 @@ pub struct StreamedContent { pub rx: CuttleReceiver, pub status: StreamStatus, pub last_packet: Option>, + pub last_packet_time: Instant, +} + +impl StreamedContent { + pub fn new(txconfig: &'static [u8], rx: CuttleReceiver) -> Self { + StreamedContent { + txconfig, + rx, + status: StreamStatus::Paused, + last_packet: None, + last_packet_time: Instant::now() - Duration::from_secs(1), + } + } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/src/main.rs b/src/main.rs index c8ea841..06239f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,7 @@ -use std::{ - ffi::OsString, - time::{Duration, Instant}, -}; +use std::ffi::OsString; use clap::Parser; -use cuttle::{stream_bytes, Content, Flasher, StreamStatus, StreamedContent}; +use cuttle::{get_content, Flasher}; use eframe::egui; #[derive(Parser, Debug)] @@ -13,8 +10,8 @@ struct Cli { #[clap(long, short, help = "File to expose")] pub file: Option, - #[clap(long, help = "Frames per second", default_value_t = 10)] - pub fps: u64, + #[clap(long, help = "Frames per second", default_value_t = 10.0)] + pub fps: f64, #[clap( help = "all remaining arguments treated as a string; this string is the whole message if `-f` is not given, otherwise it's an optional description of the file" @@ -45,18 +42,16 @@ fn main() -> Result<(), eframe::Error> { "text message".to_string() }; - let content = get_content(&cli, &description); - - let sleep = 1000.0 / cli.fps as f64; - let sleep = Duration::from_millis(sleep as u64); - let last = Instant::now(); - let flasher = Flasher { - heading: description, - content, - sleep, - last, + let bytes = if let Some(ref file) = cli.file { + std::fs::read(file).unwrap_or_else(|e| panic!("tried to open {file:?}, got {e:?}")) + } else { + cli.text().join(" ").bytes().collect() }; + let content = get_content(bytes, &description); + + let flasher = Flasher::new(description, content, cli.fps); + let options = eframe::NativeOptions { initial_window_size: Some(egui::vec2(1200.0, 1200.0)), active: true, @@ -70,27 +65,3 @@ fn main() -> Result<(), eframe::Error> { Box::new(move |_cc| Box::new(flasher)), ) } - -fn get_content(cli: &Cli, desc: &str) -> Content { - let bytes = if let Some(ref file) = cli.file { - std::fs::read(file).unwrap_or_else(|e| panic!("tried to open {file:?}, got {e:?}")) - } else { - cli.text().join(" ").bytes().collect() - }; - - if bytes.len() < 2000 && fast_qr::QRBuilder::new(bytes.clone()).build().is_ok() { - let bytes = bytes.leak(); - Content::Static(bytes) - } else { - let (tx, rx) = std::sync::mpsc::sync_channel(2); - let txconfig = stream_bytes(bytes, tx, desc.to_string()).leak(); - - let stream = StreamedContent { - txconfig, - rx, - status: StreamStatus::Paused, - last_packet: None, - }; - Content::Streamed(stream) - } -} diff --git a/src/util.rs b/src/util.rs index 269917e..4f71d4e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -2,7 +2,7 @@ use fast_qr::convert::image::ImageBuilder; use rand::seq::SliceRandom; use raptorq::{Encoder, ObjectTransmissionInformation}; -use crate::{CuttleSender, TxConfig}; +use crate::{Content, CuttleSender, StreamedContent, TxConfig}; pub const STREAMING_MTU: u16 = 2326; @@ -50,3 +50,17 @@ pub fn mk_qr_bytes(bytes: &[u8], height: f32) -> Vec { .encode_png() .unwrap() } + +/// Turns bytes and a description into either a single QR code, or a stream of +/// them, depending on the size of the input. +pub fn get_content(bytes: Vec, desc: &str) -> Content { + if bytes.len() < 2000 && fast_qr::QRBuilder::new(bytes.clone()).build().is_ok() { + let bytes = bytes.leak(); + Content::Static(bytes) + } else { + let (tx, rx) = std::sync::mpsc::sync_channel(2); + let txconfig = stream_bytes(bytes, tx, desc.to_string()).leak(); + let stream = StreamedContent::new(txconfig, rx); + Content::Streamed(stream) + } +}