minor re-org and tidy
This commit is contained in:
parent
02d5e57eed
commit
c579cc0851
5 changed files with 187 additions and 167 deletions
122
src/desktop.rs
122
src/desktop.rs
|
@ -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<StreamStatus>,
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
133
src/desktop/mod.rs
Normal file
133
src/desktop/mod.rs
Normal file
|
@ -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<StreamStatus>,
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
30
src/lib.rs
30
src/lib.rs
|
@ -9,7 +9,7 @@ use rkyv::{Archive, Deserialize, Serialize};
|
||||||
mod desktop;
|
mod desktop;
|
||||||
mod util;
|
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<Vec<u8>>;
|
pub type CuttleSender = std::sync::mpsc::SyncSender<Vec<u8>>;
|
||||||
pub type CuttleReceiver = std::sync::mpsc::Receiver<Vec<u8>>;
|
pub type CuttleReceiver = std::sync::mpsc::Receiver<Vec<u8>>;
|
||||||
|
@ -17,10 +17,21 @@ pub type CuttleReceiver = std::sync::mpsc::Receiver<Vec<u8>>;
|
||||||
/// The application state
|
/// The application state
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Flasher {
|
pub struct Flasher {
|
||||||
pub heading: String,
|
pub description: String,
|
||||||
pub content: Content,
|
pub content: Content,
|
||||||
pub sleep: Duration,
|
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)]
|
#[derive(Debug)]
|
||||||
|
@ -35,6 +46,19 @@ pub struct StreamedContent {
|
||||||
pub rx: CuttleReceiver,
|
pub rx: CuttleReceiver,
|
||||||
pub status: StreamStatus,
|
pub status: StreamStatus,
|
||||||
pub last_packet: Option<Vec<u8>>,
|
pub last_packet: Option<Vec<u8>>,
|
||||||
|
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)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
|
53
src/main.rs
53
src/main.rs
|
@ -1,10 +1,7 @@
|
||||||
use std::{
|
use std::ffi::OsString;
|
||||||
ffi::OsString,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cuttle::{stream_bytes, Content, Flasher, StreamStatus, StreamedContent};
|
use cuttle::{get_content, Flasher};
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
|
@ -13,8 +10,8 @@ struct Cli {
|
||||||
#[clap(long, short, help = "File to expose")]
|
#[clap(long, short, help = "File to expose")]
|
||||||
pub file: Option<OsString>,
|
pub file: Option<OsString>,
|
||||||
|
|
||||||
#[clap(long, help = "Frames per second", default_value_t = 10)]
|
#[clap(long, help = "Frames per second", default_value_t = 10.0)]
|
||||||
pub fps: u64,
|
pub fps: f64,
|
||||||
|
|
||||||
#[clap(
|
#[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"
|
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()
|
"text message".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let content = get_content(&cli, &description);
|
let bytes = if let Some(ref file) = cli.file {
|
||||||
|
std::fs::read(file).unwrap_or_else(|e| panic!("tried to open {file:?}, got {e:?}"))
|
||||||
let sleep = 1000.0 / cli.fps as f64;
|
} else {
|
||||||
let sleep = Duration::from_millis(sleep as u64);
|
cli.text().join(" ").bytes().collect()
|
||||||
let last = Instant::now();
|
|
||||||
let flasher = Flasher {
|
|
||||||
heading: description,
|
|
||||||
content,
|
|
||||||
sleep,
|
|
||||||
last,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let content = get_content(bytes, &description);
|
||||||
|
|
||||||
|
let flasher = Flasher::new(description, content, cli.fps);
|
||||||
|
|
||||||
let options = eframe::NativeOptions {
|
let options = eframe::NativeOptions {
|
||||||
initial_window_size: Some(egui::vec2(1200.0, 1200.0)),
|
initial_window_size: Some(egui::vec2(1200.0, 1200.0)),
|
||||||
active: true,
|
active: true,
|
||||||
|
@ -70,27 +65,3 @@ fn main() -> Result<(), eframe::Error> {
|
||||||
Box::new(move |_cc| Box::new(flasher)),
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
16
src/util.rs
16
src/util.rs
|
@ -2,7 +2,7 @@ use fast_qr::convert::image::ImageBuilder;
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use raptorq::{Encoder, ObjectTransmissionInformation};
|
use raptorq::{Encoder, ObjectTransmissionInformation};
|
||||||
|
|
||||||
use crate::{CuttleSender, TxConfig};
|
use crate::{Content, CuttleSender, StreamedContent, TxConfig};
|
||||||
|
|
||||||
pub const STREAMING_MTU: u16 = 2326;
|
pub const STREAMING_MTU: u16 = 2326;
|
||||||
|
|
||||||
|
@ -50,3 +50,17 @@ pub fn mk_qr_bytes(bytes: &[u8], height: f32) -> Vec<u8> {
|
||||||
.encode_png()
|
.encode_png()
|
||||||
.unwrap()
|
.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<u8>, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue