split out gui options

This commit is contained in:
Vinzenz Schroeter 2025-01-26 12:40:59 +01:00
parent 31a8f6a40a
commit 3049a8bd7a
6 changed files with 143 additions and 120 deletions

57
src/cli.rs Normal file
View file

@ -0,0 +1,57 @@
use clap::Parser;
#[derive(Parser, Debug)]
pub struct Cli {
#[arg(
long,
default_value = "0.0.0.0:2342",
help = "address and port to bind to"
)]
pub bind: String,
#[arg(
short,
long,
help = "Set default log level lower. You can also change this via the RUST_LOG environment variable."
)]
pub debug: bool,
#[arg(
short,
long,
help = "The name of the font family to use. This defaults to the system monospace font."
)]
pub font: Option<String>,
#[clap(flatten)]
pub gui: GuiOptions,
}
#[derive(Parser, Debug)]
pub struct GuiOptions {
#[arg(
short,
long,
default_value_t = false,
help = "Use the red color channel"
)]
pub red: bool,
#[arg(
short,
long,
default_value_t = false,
help = "Use the green color channel"
)]
pub green: bool,
#[arg(
short,
long,
default_value_t = false,
help = "Use the blue color channel"
)]
pub blue: bool,
#[arg(
short,
long,
default_value_t = false,
help = "add spacers between tile rows to simulate gaps in real display"
)]
pub spacers: bool,
}

View file

@ -18,7 +18,17 @@ impl Cp437Font {
impl Default for Cp437Font { impl Default for Cp437Font {
fn default() -> Self { fn default() -> Self {
load_static() let mut bitmaps =
core::array::from_fn(|_| Bitmap::new(TILE_SIZE, TILE_SIZE));
for (char_code, bitmap) in bitmaps.iter_mut().enumerate() {
let bits = CP437_FONT_LINEAR[char_code];
let mut bytes = bits.to_be_bytes();
bytes.reverse();
bitmap.data_ref_mut().copy_from_slice(bytes.as_slice());
}
Self::new(bitmaps)
} }
} }
@ -281,17 +291,3 @@ pub(crate) const CP437_FONT_LINEAR: [u64; 256] = [
0x00007c7c7c7c7c00, // 0xfe 0x00007c7c7c7c7c00, // 0xfe
0x0000000000000000, // 0xff 0x0000000000000000, // 0xff
]; ];
fn load_static() -> Cp437Font {
let mut bitmaps =
core::array::from_fn(|_| Bitmap::new(TILE_SIZE, TILE_SIZE));
for (char_code, bitmap) in bitmaps.iter_mut().enumerate() {
let bits = CP437_FONT_LINEAR[char_code];
let mut bytes = bits.to_be_bytes();
bytes.reverse();
bitmap.data_ref_mut().copy_from_slice(bytes.as_slice());
}
Cp437Font::new(bitmaps)
}

View file

@ -1,5 +1,5 @@
use crate::execute_command::ExecutionResult::{Failure, Shutdown, Success}; use crate::execute_command::ExecutionResult::{Failure, Shutdown, Success};
use crate::font::Cp437Font; use crate::cp437_font::Cp437Font;
use crate::font_renderer::FontRenderer8x8; use crate::font_renderer::FontRenderer8x8;
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
use servicepoint::{ use servicepoint::{
@ -13,7 +13,7 @@ pub struct CommandExecutor<'t> {
display: &'t RwLock<Bitmap>, display: &'t RwLock<Bitmap>,
luma: &'t RwLock<BrightnessGrid>, luma: &'t RwLock<BrightnessGrid>,
cp437_font: Cp437Font, cp437_font: Cp437Font,
utf8_font: FontRenderer8x8, font_renderer: FontRenderer8x8,
} }
#[must_use] #[must_use]
@ -27,11 +27,12 @@ impl<'t> CommandExecutor<'t> {
pub fn new( pub fn new(
display: &'t RwLock<Bitmap>, display: &'t RwLock<Bitmap>,
luma: &'t RwLock<BrightnessGrid>, luma: &'t RwLock<BrightnessGrid>,
font_renderer: FontRenderer8x8,
) -> Self { ) -> Self {
CommandExecutor { CommandExecutor {
display, display,
luma, luma,
utf8_font: FontRenderer8x8::default(), font_renderer,
cp437_font: Cp437Font::default(), cp437_font: Cp437Font::default(),
} }
} }
@ -190,7 +191,7 @@ impl<'t> CommandExecutor<'t> {
let tile_x = char_x + x; let tile_x = char_x + x;
let tile_y = char_y + y; let tile_y = char_y + y;
if let Err(e) = self.utf8_font.render( if let Err(e) = self.font_renderer.render(
char, char,
&mut display, &mut display,
Origin::new(tile_x * TILE_SIZE, tile_y * TILE_SIZE), Origin::new(tile_x * TILE_SIZE, tile_y * TILE_SIZE),

View file

@ -43,12 +43,13 @@ pub enum RenderError {
} }
impl FontRenderer8x8 { impl FontRenderer8x8 {
pub fn new(font: Font, fallback_char: Option<char>) -> Self { const FALLBACK_CHAR: char = '?';
pub fn new(font: Font) -> Self {
let canvas = let canvas =
Canvas::new(vec2i(TILE_SIZE as i32, TILE_SIZE as i32), Format::A8); Canvas::new(vec2i(TILE_SIZE as i32, TILE_SIZE as i32), Format::A8);
assert_eq!(canvas.pixels.len(), TILE_SIZE * TILE_SIZE); assert_eq!(canvas.pixels.len(), TILE_SIZE * TILE_SIZE);
assert_eq!(canvas.stride, TILE_SIZE); assert_eq!(canvas.stride, TILE_SIZE);
let fallback_char = fallback_char.and_then(|c| font.glyph_for_char(c)); let fallback_char = font.glyph_for_char(Self::FALLBACK_CHAR);
let result = Self { let result = Self {
font: SendFont(font), font: SendFont(font),
fallback_char, fallback_char,
@ -57,6 +58,15 @@ impl FontRenderer8x8 {
result result
} }
pub fn from_name(family_name: String) -> Self {
let font = SystemSource::new()
.select_best_match(&[FamilyName::Title(family_name)], &Properties::new())
.unwrap()
.load()
.unwrap();
Self::new(font)
}
pub fn render( pub fn render(
&self, &self,
char: char, char: char,
@ -104,17 +114,10 @@ impl FontRenderer8x8 {
impl Default for FontRenderer8x8 { impl Default for FontRenderer8x8 {
fn default() -> Self { fn default() -> Self {
let utf8_font = SystemSource::new() let utf8_font = SystemSource::new()
.select_best_match( .select_best_match(&[FamilyName::Monospace], &Properties::new())
&[
FamilyName::Title("Roboto Mono".to_string()),
FamilyName::Title("Fira Mono".to_string()),
FamilyName::Monospace,
],
&Properties::new(),
)
.unwrap() .unwrap()
.load() .load()
.unwrap(); .unwrap();
FontRenderer8x8::new(utf8_font, Some('?')) FontRenderer8x8::new(utf8_font)
} }
} }

View file

@ -12,14 +12,14 @@ use winit::event_loop::ActiveEventLoop;
use winit::keyboard::KeyCode::KeyC; use winit::keyboard::KeyCode::KeyC;
use winit::window::{Window, WindowId}; use winit::window::{Window, WindowId};
use crate::Cli; use crate::cli::{GuiOptions};
pub struct Gui<'t> { pub struct Gui<'t> {
display: &'t RwLock<Bitmap>, display: &'t RwLock<Bitmap>,
luma: &'t RwLock<BrightnessGrid>, luma: &'t RwLock<BrightnessGrid>,
window: Option<Window>, window: Option<Window>,
stop_udp_tx: Sender<()>, stop_udp_tx: Sender<()>,
cli: &'t Cli, options: GuiOptions,
logical_size: LogicalSize<u16>, logical_size: LogicalSize<u16>,
} }
@ -41,15 +41,15 @@ impl<'t> Gui<'t> {
display: &'t RwLock<Bitmap>, display: &'t RwLock<Bitmap>,
luma: &'t RwLock<BrightnessGrid>, luma: &'t RwLock<BrightnessGrid>,
stop_udp_tx: Sender<()>, stop_udp_tx: Sender<()>,
cli: &'t Cli, options: GuiOptions,
) -> Self { ) -> Self {
Gui { Gui {
window: None,
logical_size: Self::get_logical_size(options.spacers),
display, display,
luma, luma,
stop_udp_tx, stop_udp_tx,
cli, options,
window: None,
logical_size: Self::get_logical_size(cli.spacers),
} }
} }
@ -79,7 +79,7 @@ impl<'t> Gui<'t> {
let brightness_scale = (u8::MAX as f32) / (u8::from(Brightness::MAX) as f32); let brightness_scale = (u8::MAX as f32) / (u8::from(Brightness::MAX) as f32);
for tile_y in 0..TILE_HEIGHT { for tile_y in 0..TILE_HEIGHT {
if self.cli.spacers && tile_y != 0 { if self.options.spacers && tile_y != 0 {
// cannot just frame.skip(PIXEL_WIDTH as usize * SPACER_HEIGHT as usize) because of typing // cannot just frame.skip(PIXEL_WIDTH as usize * SPACER_HEIGHT as usize) because of typing
for _ in 0..PIXEL_WIDTH * SPACER_HEIGHT { for _ in 0..PIXEL_WIDTH * SPACER_HEIGHT {
frame.next().unwrap(); frame.next().unwrap();
@ -105,9 +105,9 @@ impl<'t> Gui<'t> {
fn get_on_color(&self, brightness: u8) -> [u8; 4] { fn get_on_color(&self, brightness: u8) -> [u8; 4] {
[ [
if self.cli.red { brightness } else { 0u8 }, if self.options.red { brightness } else { 0u8 },
if self.cli.green { brightness } else { 0u8 }, if self.options.green { brightness } else { 0u8 },
if self.cli.blue { brightness } else { 0u8 }, if self.options.blue { brightness } else { 0u8 },
255, 255,
] ]
} }

View file

@ -11,89 +11,37 @@ use std::io::ErrorKind;
use std::net::UdpSocket; use std::net::UdpSocket;
use std::sync::{mpsc, RwLock}; use std::sync::{mpsc, RwLock};
use std::time::Duration; use std::time::Duration;
use winit::event_loop::{ControlFlow, EventLoop}; use winit::event_loop::{ControlFlow, EventLoop, EventLoopProxy};
use cli::Cli;
use crate::font_renderer::FontRenderer8x8;
mod execute_command; mod execute_command;
mod font; mod cp437_font;
mod font_renderer; mod font_renderer;
mod gui; mod gui;
mod cli;
#[derive(Parser, Debug)]
struct Cli {
#[arg(
long,
default_value = "0.0.0.0:2342",
help = "address and port to bind to"
)]
bind: String,
#[arg(
short,
long,
default_value_t = false,
help = "add spacers between tile rows to simulate gaps in real display"
)]
spacers: bool,
#[arg(
short,
long,
help = "Set default log level lower. You can also change this via the RUST_LOG environment variable."
)]
debug: bool,
#[arg(
short,
long,
default_value_t = false,
help = "Use the red color channel"
)]
red: bool,
#[arg(
short,
long,
default_value_t = false,
help = "Use the green color channel"
)]
green: bool,
#[arg(
short,
long,
default_value_t = false,
help = "Use the blue color channel"
)]
blue: bool,
}
const BUF_SIZE: usize = 8985; const BUF_SIZE: usize = 8985;
fn main() { fn main() {
let mut cli = Cli::parse(); let mut cli = Cli::parse();
if !(cli.gui.red || cli.gui.blue || cli.gui.green) {
env_logger::builder() cli.gui.green = true;
.filter_level(if cli.debug {
LevelFilter::Debug
} else {
LevelFilter::Info
})
.parse_default_env()
.init();
if !(cli.red || cli.blue || cli.green) {
cli.green = true;
} }
init_logging(cli.debug);
info!("starting with args: {:?}", &cli); info!("starting with args: {:?}", &cli);
let socket = UdpSocket::bind(&cli.bind).expect("could not bind socket"); let socket = UdpSocket::bind(&cli.bind).expect("could not bind socket");
socket socket
.set_nonblocking(true) .set_nonblocking(true)
.expect("could not enter non blocking mode"); .expect("could not enter non blocking mode");
let display = RwLock::new(Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT)); let display = RwLock::new(Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT));
let luma = RwLock::new(BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT));
let mut luma = BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT);
luma.fill(Brightness::MAX);
let luma = RwLock::new(luma);
let (stop_udp_tx, stop_udp_rx) = mpsc::channel(); let (stop_udp_tx, stop_udp_rx) = mpsc::channel();
let mut app = Gui::new(&display, &luma, stop_udp_tx, &cli); let mut gui = Gui::new(&display, &luma, stop_udp_tx, cli.gui);
let event_loop = EventLoop::with_user_event() let event_loop = EventLoop::with_user_event()
.build() .build()
@ -101,7 +49,9 @@ fn main() {
event_loop.set_control_flow(ControlFlow::Wait); event_loop.set_control_flow(ControlFlow::Wait);
let event_proxy = event_loop.create_proxy(); let event_proxy = event_loop.create_proxy();
let command_executor = CommandExecutor::new(&display, &luma); let font_renderer = cli.font.map(move |font| FontRenderer8x8::from_name(font))
.unwrap_or_else(move || FontRenderer8x8::default());
let command_executor = CommandExecutor::new(&display, &luma, font_renderer);
std::thread::scope(move |scope| { std::thread::scope(move |scope| {
scope.spawn(move || { scope.spawn(move || {
@ -117,6 +67,16 @@ fn main() {
None => continue, None => continue,
}; };
handle_command(&event_proxy, &command_executor, command);
}
});
event_loop
.run_app(&mut gui)
.expect("could not run event loop");
});
}
fn handle_command(event_proxy: &EventLoopProxy<AppEvents>, command_executor: &CommandExecutor, command: Command) {
match command_executor.execute(command) { match command_executor.execute(command) {
ExecutionResult::Success => { ExecutionResult::Success => {
event_proxy event_proxy
@ -133,11 +93,17 @@ fn main() {
} }
} }
} }
});
event_loop fn init_logging(debug: bool) {
.run_app(&mut app) let filter = if debug {
.expect("could not run event loop"); LevelFilter::Debug
}); } else {
LevelFilter::Info
};
env_logger::builder()
.filter_level(filter)
.parse_default_env()
.init();
} }
fn command_from_slice(slice: &[u8]) -> Option<Command> { fn command_from_slice(slice: &[u8]) -> Option<Command> {