Feat: fixing ratatui big-text and UX
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s

This commit is contained in:
2026-02-16 15:43:22 +01:00
parent 211e71f5a9
commit 5385bf675a
11 changed files with 526 additions and 151 deletions

View File

@@ -0,0 +1,267 @@
//! Programmatic rendering of Unicode block elements for the desktop backend.
//!
//! Real terminals render block characters (█, ▀, ▄, quadrants, sextants) as
//! pixel-perfect filled rectangles. The bitmap font backend can't guarantee
//! gap-free fills and lacks sextant glyphs entirely. This wrapper intercepts
//! block element code points and draws them directly on the pixmap, delegating
//! everything else to EmbeddedGraphics.
use ratatui::buffer::Cell;
use ratatui::style::{Color, Modifier};
use rustc_hash::FxHashSet;
use soft_ratatui::{EmbeddedGraphics, RasterBackend, RgbPixmap};
pub struct BlockCharBackend {
pub inner: EmbeddedGraphics,
}
impl RasterBackend for BlockCharBackend {
fn draw_cell(
&mut self,
x: u16,
y: u16,
cell: &Cell,
always_redraw_list: &mut FxHashSet<(u16, u16)>,
blinking_fast: bool,
blinking_slow: bool,
char_width: usize,
char_height: usize,
rgb_pixmap: &mut RgbPixmap,
) {
let cp = cell.symbol().chars().next().unwrap_or(' ') as u32;
if !is_block_element(cp) {
self.inner.draw_cell(
x,
y,
cell,
always_redraw_list,
blinking_fast,
blinking_slow,
char_width,
char_height,
rgb_pixmap,
);
return;
}
let (fg, bg) = resolve_colors(cell, always_redraw_list, x, y, blinking_fast, blinking_slow);
let px = x as usize * char_width;
let py = y as usize * char_height;
fill_rect(rgb_pixmap, px, py, char_width, char_height, bg);
draw_block_element(rgb_pixmap, cp, px, py, char_width, char_height, fg);
}
}
// ---------------------------------------------------------------------------
// Block element classification and drawing
// ---------------------------------------------------------------------------
fn is_block_element(cp: u32) -> bool {
matches!(cp, 0x2580..=0x2590 | 0x2594..=0x259F | 0x1FB00..=0x1FB3B)
}
fn draw_block_element(
pixmap: &mut RgbPixmap,
cp: u32,
px: usize,
py: usize,
cw: usize,
ch: usize,
fg: [u8; 3],
) {
match cp {
0x2580 => fill_rect(pixmap, px, py, cw, ch / 2, fg),
0x2581..=0x2587 => {
let n = (cp - 0x2580) as usize;
let h = ch * n / 8;
fill_rect(pixmap, px, py + ch - h, cw, h, fg);
}
0x2588 => fill_rect(pixmap, px, py, cw, ch, fg),
0x2589..=0x258F => {
let n = (0x2590 - cp) as usize;
fill_rect(pixmap, px, py, cw * n / 8, ch, fg);
}
0x2590 => {
let hw = cw / 2;
fill_rect(pixmap, px + hw, py, cw - hw, ch, fg);
}
0x2594 => fill_rect(pixmap, px, py, cw, (ch / 8).max(1), fg),
0x2595 => {
let w = (cw / 8).max(1);
fill_rect(pixmap, px + cw - w, py, w, ch, fg);
}
0x2596..=0x259F => draw_quadrants(pixmap, px, py, cw, ch, fg, cp),
0x1FB00..=0x1FB3B => draw_sextants(pixmap, px, py, cw, ch, fg, cp),
_ => unreachable!(),
}
}
// ---------------------------------------------------------------------------
// Quadrants (U+2596-U+259F): 2x2 grid
// ---------------------------------------------------------------------------
// Bits: 3=UL, 2=UR, 1=LL, 0=LR
const QUADRANT: [u8; 10] = [
0b0010, // ▖ LL
0b0001, // ▗ LR
0b1000, // ▘ UL
0b1011, // ▙ UL+LL+LR
0b1001, // ▚ UL+LR
0b1110, // ▛ UL+UR+LL
0b1101, // ▜ UL+UR+LR
0b0100, // ▝ UR
0b0110, // ▞ UR+LL
0b0111, // ▟ UR+LL+LR
];
fn draw_quadrants(
pixmap: &mut RgbPixmap,
px: usize,
py: usize,
cw: usize,
ch: usize,
fg: [u8; 3],
cp: u32,
) {
let pattern = QUADRANT[(cp - 0x2596) as usize];
let hw = cw / 2;
let hh = ch / 2;
let rw = cw - hw;
let rh = ch - hh;
if pattern & 0b1000 != 0 { fill_rect(pixmap, px, py, hw, hh, fg); }
if pattern & 0b0100 != 0 { fill_rect(pixmap, px + hw, py, rw, hh, fg); }
if pattern & 0b0010 != 0 { fill_rect(pixmap, px, py + hh, hw, rh, fg); }
if pattern & 0b0001 != 0 { fill_rect(pixmap, px + hw, py + hh, rw, rh, fg); }
}
// ---------------------------------------------------------------------------
// Sextants (U+1FB00-U+1FB3B): 2x3 grid
// ---------------------------------------------------------------------------
// Bit layout: 0=TL, 1=TR, 2=ML, 3=MR, 4=BL, 5=BR
// The 60 characters encode patterns 1-62, skipping 0 (space), 21 (left half),
// 42 (right half), and 63 (full block) which exist as standard block elements.
fn sextant_pattern(cp: u32) -> u8 {
let mut p = (cp - 0x1FB00) as u8 + 1;
if p >= 21 { p += 1; }
if p >= 42 { p += 1; }
p
}
fn draw_sextants(
pixmap: &mut RgbPixmap,
px: usize,
py: usize,
cw: usize,
ch: usize,
fg: [u8; 3],
cp: u32,
) {
let pattern = sextant_pattern(cp);
let hw = cw / 2;
let rw = cw - hw;
let h0 = ch / 3;
let h1 = (ch - h0) / 2;
let h2 = ch - h0 - h1;
let y1 = py + h0;
let y2 = y1 + h1;
if pattern & 0b000001 != 0 { fill_rect(pixmap, px, py, hw, h0, fg); }
if pattern & 0b000010 != 0 { fill_rect(pixmap, px + hw, py, rw, h0, fg); }
if pattern & 0b000100 != 0 { fill_rect(pixmap, px, y1, hw, h1, fg); }
if pattern & 0b001000 != 0 { fill_rect(pixmap, px + hw, y1, rw, h1, fg); }
if pattern & 0b010000 != 0 { fill_rect(pixmap, px, y2, hw, h2, fg); }
if pattern & 0b100000 != 0 { fill_rect(pixmap, px + hw, y2, rw, h2, fg); }
}
// ---------------------------------------------------------------------------
// Pixel operations
// ---------------------------------------------------------------------------
fn fill_rect(pixmap: &mut RgbPixmap, x0: usize, y0: usize, w: usize, h: usize, color: [u8; 3]) {
let pw = pixmap.width;
let x_end = (x0 + w).min(pw);
let y_end = (y0 + h).min(pixmap.height);
let data = &mut pixmap.data;
for y in y0..y_end {
let start = 3 * (y * pw + x0);
let end = 3 * (y * pw + x_end);
for chunk in data[start..end].chunks_exact_mut(3) {
chunk.copy_from_slice(&color);
}
}
}
// ---------------------------------------------------------------------------
// Color resolution (mirrors soft_ratatui::colors which is private)
// ---------------------------------------------------------------------------
fn resolve_colors(
cell: &Cell,
always_redraw_list: &mut FxHashSet<(u16, u16)>,
x: u16,
y: u16,
blinking_fast: bool,
blinking_slow: bool,
) -> ([u8; 3], [u8; 3]) {
let mut fg = color_to_rgb(&cell.fg, true);
let mut bg = color_to_rgb(&cell.bg, false);
for modifier in cell.modifier.iter() {
match modifier {
Modifier::DIM => {
fg = dim_rgb(fg);
bg = dim_rgb(bg);
}
Modifier::REVERSED => std::mem::swap(&mut fg, &mut bg),
Modifier::HIDDEN => fg = bg,
Modifier::SLOW_BLINK => {
always_redraw_list.insert((x, y));
if blinking_slow { fg = bg; }
}
Modifier::RAPID_BLINK => {
always_redraw_list.insert((x, y));
if blinking_fast { fg = bg; }
}
_ => {}
}
}
(fg, bg)
}
fn color_to_rgb(color: &Color, is_fg: bool) -> [u8; 3] {
match color {
Color::Reset if is_fg => [204, 204, 255],
Color::Reset => [5, 1, 121],
Color::Black => [0, 0, 0],
Color::Red => [139, 0, 0],
Color::Green => [0, 100, 0],
Color::Yellow => [255, 215, 0],
Color::Blue => [0, 0, 139],
Color::Magenta => [255, 0, 255],
Color::Cyan => [0, 0, 255],
Color::Gray => [128, 128, 128],
Color::DarkGray => [64, 64, 64],
Color::LightRed => [255, 0, 0],
Color::LightGreen => [0, 255, 0],
Color::LightBlue => [173, 216, 230],
Color::LightYellow => [255, 255, 224],
Color::LightMagenta => [139, 0, 139],
Color::LightCyan => [224, 255, 255],
Color::White => [255, 255, 255],
Color::Indexed(i) => [i.wrapping_mul(*i), i.wrapping_add(*i), *i],
Color::Rgb(r, g, b) => [*r, *g, *b],
}
}
fn dim_rgb(c: [u8; 3]) -> [u8; 3] {
const F: u32 = 77; // ~30% brightness
[
((c[0] as u32 * F + 127) / 255) as u8,
((c[1] as u32 * F + 127) / 255) as u8,
((c[2] as u32 * F + 127) / 255) as u8,
]
}

603
src/bin/desktop/main.rs Normal file
View File

@@ -0,0 +1,603 @@
mod block_renderer;
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use block_renderer::BlockCharBackend;
use clap::Parser;
use doux::EngineMetrics;
use eframe::NativeOptions;
use egui_ratatui::RataguiBackend;
use ratatui::Terminal;
use soft_ratatui::embedded_graphics_unicodefonts::{
mono_10x20_atlas, mono_6x13_atlas, mono_6x13_bold_atlas, mono_6x13_italic_atlas,
mono_7x13_atlas, mono_7x13_bold_atlas, mono_7x13_italic_atlas, mono_8x13_atlas,
mono_8x13_bold_atlas, mono_8x13_italic_atlas, mono_9x15_atlas, mono_9x15_bold_atlas,
mono_9x18_atlas, mono_9x18_bold_atlas,
};
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
use cagire::engine::{
build_stream, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand, ScopeBuffer,
SequencerHandle, SpectrumBuffer,
};
use cagire::init::{init, InitArgs};
use cagire::input::{handle_key, handle_mouse, InputContext, InputResult};
use cagire::input_egui::{convert_egui_events, convert_egui_mouse};
use cagire::settings::Settings;
use cagire::views;
use crossbeam_channel::Receiver;
#[derive(Parser)]
#[command(name = "cagire-desktop", about = "Cagire desktop application")]
struct Args {
#[arg(short, long)]
samples: Vec<std::path::PathBuf>,
#[arg(short, long)]
output: Option<String>,
#[arg(short, long)]
input: Option<String>,
#[arg(short, long)]
channels: Option<u16>,
#[arg(short, long)]
buffer: Option<u32>,
}
#[derive(Clone, Copy, PartialEq)]
enum FontChoice {
Size6x13,
Size7x13,
Size8x13,
Size9x15,
Size9x18,
Size10x20,
}
impl FontChoice {
fn from_setting(s: &str) -> Self {
match s {
"6x13" => Self::Size6x13,
"7x13" => Self::Size7x13,
"9x15" => Self::Size9x15,
"9x18" => Self::Size9x18,
"10x20" => Self::Size10x20,
_ => Self::Size8x13,
}
}
fn to_setting(self) -> &'static str {
match self {
Self::Size6x13 => "6x13",
Self::Size7x13 => "7x13",
Self::Size8x13 => "8x13",
Self::Size9x15 => "9x15",
Self::Size9x18 => "9x18",
Self::Size10x20 => "10x20",
}
}
fn label(self) -> &'static str {
match self {
Self::Size6x13 => "6x13 (Compact)",
Self::Size7x13 => "7x13",
Self::Size8x13 => "8x13 (Default)",
Self::Size9x15 => "9x15",
Self::Size9x18 => "9x18",
Self::Size10x20 => "10x20 (Large)",
}
}
const ALL: [Self; 6] = [
Self::Size6x13,
Self::Size7x13,
Self::Size8x13,
Self::Size9x15,
Self::Size9x18,
Self::Size10x20,
];
}
type TerminalType = Terminal<RataguiBackend<BlockCharBackend>>;
fn create_terminal(font: FontChoice) -> TerminalType {
let (regular, bold, italic) = match font {
FontChoice::Size6x13 => (
mono_6x13_atlas(),
Some(mono_6x13_bold_atlas()),
Some(mono_6x13_italic_atlas()),
),
FontChoice::Size7x13 => (
mono_7x13_atlas(),
Some(mono_7x13_bold_atlas()),
Some(mono_7x13_italic_atlas()),
),
FontChoice::Size8x13 => (
mono_8x13_atlas(),
Some(mono_8x13_bold_atlas()),
Some(mono_8x13_italic_atlas()),
),
FontChoice::Size9x15 => (mono_9x15_atlas(), Some(mono_9x15_bold_atlas()), None),
FontChoice::Size9x18 => (mono_9x18_atlas(), Some(mono_9x18_bold_atlas()), None),
FontChoice::Size10x20 => (mono_10x20_atlas(), None, None),
};
let eg = SoftBackend::<EmbeddedGraphics>::new(80, 24, regular, bold, italic);
let soft = SoftBackend {
buffer: eg.buffer,
cursor: eg.cursor,
cursor_pos: eg.cursor_pos,
char_width: eg.char_width,
char_height: eg.char_height,
blink_counter: eg.blink_counter,
blinking_fast: eg.blinking_fast,
blinking_slow: eg.blinking_slow,
rgb_pixmap: eg.rgb_pixmap,
always_redraw_list: eg.always_redraw_list,
raster_backend: BlockCharBackend {
inner: eg.raster_backend,
},
};
Terminal::new(RataguiBackend::new("cagire", soft)).expect("terminal")
}
struct CagireDesktop {
app: cagire::app::App,
terminal: TerminalType,
link: Arc<LinkState>,
sequencer: Option<SequencerHandle>,
playing: Arc<AtomicBool>,
nudge_us: Arc<AtomicI64>,
metrics: Arc<EngineMetrics>,
scope_buffer: Arc<ScopeBuffer>,
spectrum_buffer: Arc<SpectrumBuffer>,
audio_sample_pos: Arc<AtomicU64>,
sample_rate_shared: Arc<AtomicU32>,
_stream: Option<cpal::Stream>,
_analysis_handle: Option<AnalysisHandle>,
midi_rx: Receiver<MidiCommand>,
current_font: FontChoice,
zoom_factor: f32,
fullscreen: bool,
decorations: bool,
always_on_top: bool,
mouse_x: Arc<AtomicU32>,
mouse_y: Arc<AtomicU32>,
mouse_down: Arc<AtomicU32>,
last_frame: std::time::Instant,
}
impl CagireDesktop {
fn new(cc: &eframe::CreationContext<'_>, args: Args) -> Self {
let b = init(InitArgs {
samples: args.samples,
output: args.output,
input: args.input,
channels: args.channels,
buffer: args.buffer,
});
let current_font = FontChoice::from_setting(&b.settings.display.font);
let terminal = create_terminal(current_font);
let zoom_factor = b.settings.display.zoom_factor;
cc.egui_ctx.set_visuals(egui::Visuals::dark());
cc.egui_ctx.set_zoom_factor(zoom_factor);
Self {
app: b.app,
terminal,
link: b.link,
sequencer: Some(b.sequencer),
playing: b.playing,
nudge_us: b.nudge_us,
metrics: b.metrics,
scope_buffer: b.scope_buffer,
spectrum_buffer: b.spectrum_buffer,
audio_sample_pos: b.audio_sample_pos,
sample_rate_shared: b.sample_rate_shared,
_stream: b.stream,
_analysis_handle: b.analysis_handle,
midi_rx: b.midi_rx,
current_font,
zoom_factor,
fullscreen: false,
decorations: true,
always_on_top: false,
mouse_x: b.mouse_x,
mouse_y: b.mouse_y,
mouse_down: b.mouse_down,
last_frame: std::time::Instant::now(),
}
}
fn handle_audio_restart(&mut self) {
if !self.app.audio.restart_pending {
return;
}
self.app.audio.restart_pending = false;
self._stream = None;
self._analysis_handle = None;
let Some(ref sequencer) = self.sequencer else {
return;
};
let new_audio_rx = sequencer.swap_audio_channel();
self.midi_rx = sequencer.swap_midi_channel();
let new_config = AudioStreamConfig {
output_device: self.app.audio.config.output_device.clone(),
channels: self.app.audio.config.channels,
buffer_size: self.app.audio.config.buffer_size,
max_voices: self.app.audio.config.max_voices,
};
let mut restart_samples = Vec::new();
for path in &self.app.audio.config.sample_paths {
let index = doux::sampling::scan_samples_dir(path);
restart_samples.extend(index);
}
self.app.audio.config.sample_count = restart_samples.len();
self.audio_sample_pos.store(0, Ordering::Release);
let preload_entries: Vec<(String, std::path::PathBuf)> = restart_samples
.iter()
.map(|e| (e.name.clone(), e.path.clone()))
.collect();
match build_stream(
&new_config,
new_audio_rx,
Arc::clone(&self.scope_buffer),
Arc::clone(&self.spectrum_buffer),
Arc::clone(&self.metrics),
restart_samples,
Arc::clone(&self.audio_sample_pos),
) {
Ok((new_stream, info, new_analysis, registry)) => {
self._stream = Some(new_stream);
self._analysis_handle = Some(new_analysis);
self.app.audio.config.sample_rate = info.sample_rate;
self.app.audio.config.host_name = info.host_name;
self.app.audio.config.channels = info.channels;
self.sample_rate_shared
.store(info.sample_rate as u32, Ordering::Relaxed);
self.app.audio.error = None;
self.app.audio.sample_registry = Some(std::sync::Arc::clone(&registry));
self.app.ui.set_status("Audio restarted".to_string());
if !preload_entries.is_empty() {
let sr = info.sample_rate;
std::thread::Builder::new()
.name("sample-preload".into())
.spawn(move || {
cagire::init::preload_sample_heads(preload_entries, sr, &registry);
})
.expect("failed to spawn preload thread");
}
}
Err(e) => {
self.app.audio.error = Some(e.clone());
self.app.ui.set_status(format!("Audio failed: {e}"));
}
}
}
fn update_metrics(&mut self) {
self.app.playback.playing = self.playing.load(Ordering::Relaxed);
self.app.metrics.active_voices =
self.metrics.active_voices.load(Ordering::Relaxed) as usize;
self.app.metrics.peak_voices = self
.app
.metrics
.peak_voices
.max(self.app.metrics.active_voices);
self.app.metrics.cpu_load = self.metrics.load.get_load();
self.app.metrics.schedule_depth =
self.metrics.schedule_depth.load(Ordering::Relaxed) as usize;
self.app.metrics.scope = self.scope_buffer.read();
(self.app.metrics.peak_left, self.app.metrics.peak_right) = self.scope_buffer.peaks();
self.app.metrics.spectrum = self.spectrum_buffer.read();
self.app.metrics.nudge_ms = self.nudge_us.load(Ordering::Relaxed) as f64 / 1000.0;
}
fn handle_input(&mut self, ctx: &egui::Context) -> bool {
let Some(ref sequencer) = self.sequencer else {
return false;
};
let seq_snapshot = sequencer.snapshot();
let term = self.terminal.get_frame().area();
let widget_rect = ctx.content_rect();
for mouse in convert_egui_mouse(ctx, widget_rect, term) {
let mut input_ctx = InputContext {
app: &mut self.app,
link: &self.link,
snapshot: &seq_snapshot,
playing: &self.playing,
audio_tx: &sequencer.audio_tx,
seq_cmd_tx: &sequencer.cmd_tx,
nudge_us: &self.nudge_us,
};
handle_mouse(&mut input_ctx, mouse, term);
}
for key in convert_egui_events(ctx) {
let mut input_ctx = InputContext {
app: &mut self.app,
link: &self.link,
snapshot: &seq_snapshot,
playing: &self.playing,
audio_tx: &sequencer.audio_tx,
seq_cmd_tx: &sequencer.cmd_tx,
nudge_us: &self.nudge_us,
};
if let InputResult::Quit = handle_key(&mut input_ctx, key) {
return true;
}
}
false
}
}
impl eframe::App for CagireDesktop {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.handle_audio_restart();
self.update_metrics();
ctx.input(|i| {
if let Some(pos) = i.pointer.latest_pos() {
let screen = i.viewport_rect();
let nx = (pos.x / screen.width()).clamp(0.0, 1.0);
let ny = (pos.y / screen.height()).clamp(0.0, 1.0);
self.mouse_x.store(nx.to_bits(), Ordering::Relaxed);
self.mouse_y.store(ny.to_bits(), Ordering::Relaxed);
}
let down = if i.pointer.primary_down() {
1.0_f32
} else {
0.0_f32
};
self.mouse_down.store(down.to_bits(), Ordering::Relaxed);
});
let Some(ref sequencer) = self.sequencer else {
return;
};
let seq_snapshot = sequencer.snapshot();
self.app.metrics.event_count = seq_snapshot.event_count;
self.app.flush_queued_changes(&sequencer.cmd_tx);
self.app.flush_dirty_patterns(&sequencer.cmd_tx);
while let Ok(midi_cmd) = self.midi_rx.try_recv() {
match midi_cmd {
MidiCommand::NoteOn {
device,
channel,
note,
velocity,
} => {
self.app.midi.send_note_on(device, channel, note, velocity);
}
MidiCommand::NoteOff {
device,
channel,
note,
} => {
self.app.midi.send_note_off(device, channel, note);
}
MidiCommand::CC {
device,
channel,
cc,
value,
} => {
self.app.midi.send_cc(device, channel, cc, value);
}
MidiCommand::PitchBend {
device,
channel,
value,
} => {
self.app.midi.send_pitch_bend(device, channel, value);
}
MidiCommand::Pressure {
device,
channel,
value,
} => {
self.app.midi.send_pressure(device, channel, value);
}
MidiCommand::ProgramChange {
device,
channel,
program,
} => {
self.app.midi.send_program_change(device, channel, program);
}
MidiCommand::Clock { device } => self.app.midi.send_realtime(device, 0xF8),
MidiCommand::Start { device } => self.app.midi.send_realtime(device, 0xFA),
MidiCommand::Stop { device } => self.app.midi.send_realtime(device, 0xFC),
MidiCommand::Continue { device } => self.app.midi.send_realtime(device, 0xFB),
}
}
let should_quit = self.handle_input(ctx);
if should_quit {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
return;
}
let current_font = self.current_font;
let current_zoom = self.zoom_factor;
let current_fullscreen = self.fullscreen;
let current_decorations = self.decorations;
let current_always_on_top = self.always_on_top;
let mut new_font = None;
let mut new_zoom = None;
let mut toggle_fullscreen = false;
let mut toggle_decorations = false;
let mut toggle_always_on_top = false;
egui::CentralPanel::default()
.frame(egui::Frame::NONE.fill(egui::Color32::BLACK))
.show(ctx, |ui| {
if self.app.ui.show_title {
self.app.ui.sparkles.tick(self.terminal.get_frame().area());
}
cagire::state::effects::tick_effects(&mut self.app.ui, self.app.page);
let elapsed = self.last_frame.elapsed();
self.last_frame = std::time::Instant::now();
let link = &self.link;
let app = &self.app;
self.terminal
.draw(|frame| views::render(frame, app, link, &seq_snapshot, elapsed))
.expect("Failed to draw");
ui.add(self.terminal.backend_mut());
let response = ui.interact(
ui.max_rect(),
egui::Id::new("terminal_context"),
egui::Sense::click(),
);
response.context_menu(|ui| {
ui.menu_button("Font", |ui| {
for choice in FontChoice::ALL {
let selected = current_font == choice;
if ui.selectable_label(selected, choice.label()).clicked() {
new_font = Some(choice);
ui.close();
}
}
});
ui.menu_button("Zoom", |ui| {
for &level in &[0.5_f32, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] {
let selected = (current_zoom - level).abs() < 0.01;
let label = format!("{:.0}%", level * 100.0);
if ui.selectable_label(selected, label).clicked() {
new_zoom = Some(level);
ui.close();
}
}
});
ui.separator();
if ui
.selectable_label(current_fullscreen, "Fullscreen")
.clicked()
{
toggle_fullscreen = true;
ui.close();
}
if ui
.selectable_label(current_always_on_top, "Always On Top")
.clicked()
{
toggle_always_on_top = true;
ui.close();
}
if ui
.selectable_label(!current_decorations, "Borderless")
.clicked()
{
toggle_decorations = true;
ui.close();
}
});
});
if let Some(font) = new_font {
self.terminal = create_terminal(font);
self.current_font = font;
let mut settings = Settings::load();
settings.display.font = font.to_setting().to_string();
settings.save();
}
if let Some(zoom) = new_zoom {
self.zoom_factor = zoom;
ctx.set_zoom_factor(zoom);
let mut settings = Settings::load();
settings.display.zoom_factor = zoom;
settings.save();
}
if toggle_fullscreen {
self.fullscreen = !self.fullscreen;
ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(self.fullscreen));
}
if toggle_always_on_top {
self.always_on_top = !self.always_on_top;
let level = if self.always_on_top {
egui::WindowLevel::AlwaysOnTop
} else {
egui::WindowLevel::Normal
};
ctx.send_viewport_cmd(egui::ViewportCommand::WindowLevel(level));
}
if toggle_decorations {
self.decorations = !self.decorations;
ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(self.decorations));
}
ctx.request_repaint_after(Duration::from_millis(
self.app.audio.config.refresh_rate.millis(),
));
}
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
if let Some(sequencer) = self.sequencer.take() {
sequencer.shutdown();
}
}
}
fn load_icon() -> egui::IconData {
const ICON_BYTES: &[u8] = include_bytes!("../../../assets/Cagire.png");
let img = image::load_from_memory(ICON_BYTES)
.expect("Failed to load embedded icon")
.resize(64, 64, image::imageops::FilterType::Lanczos3)
.into_rgba8();
let (width, height) = img.dimensions();
egui::IconData {
rgba: img.into_raw(),
width,
height,
}
}
fn main() -> eframe::Result<()> {
#[cfg(unix)]
cagire::engine::realtime::lock_memory();
let args = Args::parse();
let options = NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_title("Cagire")
.with_inner_size([1200.0, 800.0])
.with_icon(std::sync::Arc::new(load_icon())),
..Default::default()
};
eframe::run_native(
"Cagire",
options,
Box::new(move |cc| Ok(Box::new(CagireDesktop::new(cc, args)))),
)
}