Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
31 kB · 795 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796pub mod coordinates;pub mod dialog;pub mod draw_buffer;pub mod header;pub mod pages;pub mod render;pub mod widgets;
use std::{ collections::VecDeque, fmt::Debug, num::NonZero, sync::{Arc, LazyLock, OnceLock}, thread::JoinHandle, time::Duration,};
#[cfg(feature = "accesskit")]use accesskit::NodeId;use smol::{channel::Sender, lock::Mutex};use torque_tracker_engine::{ AudioManager, OutputConfig, PlaybackSettings, ToWorkerMsg, project::song::{Song, SongOperation},};use winit::{ application::ApplicationHandler, event::{Modifiers, WindowEvent}, event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy}, keyboard::{Key, NamedKey}, window::WindowAttributes,};
use cpal::{ BufferSize, OutputStreamTimestamp, SupportedBufferSize, traits::{DeviceTrait, HostTrait},};
use pages::{ AllPages, PageEvent, PageResponse, PagesEnum, order_list::OrderListPageEvent, pattern::PatternPageEvent,};
use crate::dialog::DialogEnum;
use { dialog::{DialogManager, DialogResponse, confirm::ConfirmDialog, page_menu::MainMenu}, draw_buffer::DrawBuffer, header::{Header, HeaderEvent},};
#[cfg(all(feature = "gpu_scaling", feature = "soft_scaling"))]use render::BothRenderBackend as RenderBackend;#[cfg(all(feature = "gpu_scaling", not(feature = "soft_scaling")))]use render::GPURenderBackend as RenderBackend;#[cfg(all(not(feature = "gpu_scaling"), feature = "soft_scaling"))]use render::SoftRenderBackend as RenderBackend;
pub static EXECUTOR: smol::Executor = smol::Executor::new();/// Song datapub static SONG_MANAGER: LazyLock<smol::lock::Mutex<AudioManager>> = LazyLock::new(|| Mutex::new(AudioManager::new(Song::default())));/// Sender for Song changespub static SONG_OP_SEND: OnceLock<smol::channel::Sender<SongOperation>> = OnceLock::new();
/// shorter function namepub fn send_song_op(op: SongOperation) { SONG_OP_SEND.get().unwrap().send_blocking(op).unwrap();}
pub enum GlobalEvent { OpenDialog(DialogEnum), Page(PageEvent), Header(HeaderEvent), /// also closes all dialogs GoToPage(PagesEnum), // Needed because only in the main app i know which pattern is selected, so i know what to play Playback(PlaybackType), #[cfg(feature = "accesskit")] Accesskit(accesskit_winit::WindowEvent), CloseRequested, CloseApp, ConstRedraw,}
impl Debug for GlobalEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut debug = f.debug_struct("GlobalEvent"); match self { GlobalEvent::OpenDialog(_) => debug.field("OpenDialog", &"closure"), GlobalEvent::Page(page_event) => debug.field("Page", page_event), GlobalEvent::Header(header_event) => debug.field("Header", header_event), GlobalEvent::GoToPage(pages_enum) => debug.field("GoToPage", pages_enum), GlobalEvent::CloseRequested => debug.field("CloseRequested", &""), GlobalEvent::CloseApp => debug.field("CloseApp", &""), GlobalEvent::ConstRedraw => debug.field("ConstRedraw", &""), GlobalEvent::Playback(playback_type) => debug.field("Playback", &playback_type), #[cfg(feature = "accesskit")] GlobalEvent::Accesskit(window_event) => debug.field("Accesskit", &window_event), }; debug.finish() }}
#[cfg(feature = "accesskit")]impl From<accesskit_winit::Event> for GlobalEvent { fn from(value: accesskit_winit::Event) -> Self { // ignore window id, because i only have one window Self::Accesskit(value.window_event) }}
#[derive(Clone, Copy, Debug)]pub enum PlaybackType { Stop, Song, Pattern, FromOrder, FromCursor,}
struct WorkerThreads { handles: [JoinHandle<()>; 2], close_msg: [Sender<()>; 2],}
impl WorkerThreads { fn new() -> Self { let (send1, recv1) = smol::channel::unbounded(); let thread1 = std::thread::Builder::new() .name("Background Worker 1".into()) .spawn(Self::worker_task(recv1)) .unwrap(); let (send2, recv2) = smol::channel::unbounded(); let thread2 = std::thread::Builder::new() .name("Background Worker 2".into()) .spawn(Self::worker_task(recv2)) .unwrap();
Self { handles: [thread1, thread2], close_msg: [send1, send2], } }
fn worker_task(recv: smol::channel::Receiver<()>) -> impl FnOnce() + Send + 'static { move || { smol::block_on(EXECUTOR.run(async { recv.recv().await.unwrap() })); } }
/// prepares the closing of the threads by signalling them to stop fn send_close(&mut self) { _ = self.close_msg[0].send_blocking(()); _ = self.close_msg[1].send_blocking(()); }
fn close_all(mut self) { self.send_close(); let [handle1, handle2] = self.handles; handle1.join().unwrap(); handle2.join().unwrap(); }}
pub struct EventQueue<'a>(&'a mut VecDeque<GlobalEvent>);
impl EventQueue<'_> { pub fn push(&mut self, event: GlobalEvent) { self.0.push_back(event); }}
/// window with all the additional stuffstruct Window { window: Arc<winit::window::Window>, render_backend: RenderBackend, #[cfg(feature = "accesskit")] adapter: (accesskit_winit::Adapter, accesskit::Affine),}
pub struct App { window: Option<Window>, draw_buffer: DrawBuffer, modifiers: Modifiers, ui_pages: AllPages, event_queue: VecDeque<GlobalEvent>, dialog_manager: DialogManager, header: Header, event_loop_proxy: EventLoopProxy<GlobalEvent>, worker_threads: Option<WorkerThreads>, audio_stream: Option<( cpal::Stream, smol::Task<()>, torque_tracker_engine::StreamSend, )>,}
impl ApplicationHandler<GlobalEvent> for App { fn new_events(&mut self, _: &ActiveEventLoop, start_cause: winit::event::StartCause) { if start_cause == winit::event::StartCause::Init { LazyLock::force(&SONG_MANAGER); self.worker_threads = Some(WorkerThreads::new()); let (send, recv) = smol::channel::unbounded(); SONG_OP_SEND.get_or_init(|| send); EXECUTOR .spawn(async move { while let Ok(op) = recv.recv().await { let mut manager = SONG_MANAGER.lock().await; // if there is no active channel the buffer isn't used, so it doesn't matter that it's wrong let buffer_time = manager.last_buffer_time(); // spin loop to lock the song let mut song = loop { if let Some(song) = manager.try_edit_song() { break song; } // smol mutex lock is held across await point smol::Timer::after(buffer_time).await; }; // apply the received op song.apply_operation(op).unwrap(); // try to get more ops. This avoids repeated locking of the song when a lot of operations are // in queue while let Ok(op) = recv.try_recv() { song.apply_operation(op).unwrap(); } drop(song); } }) .detach(); // spawn a task to collect sample garbage every 10 seconds EXECUTOR .spawn(async { loop { let mut lock = SONG_MANAGER.lock().await; lock.collect_garbage(); drop(lock); smol::Timer::after(Duration::from_secs(10)).await; } }) .detach(); self.start_audio_stream(); } }
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { self.build_window(event_loop); }
fn suspended(&mut self, _: &ActiveEventLoop) { // my window and GPU state have been invalidated self.window = None; }
fn window_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, _: winit::window::WindowId, event: WindowEvent, ) { // destructure so i don't have to always type self. let Self { window, draw_buffer, modifiers, ui_pages, event_queue, dialog_manager, header, event_loop_proxy: _, worker_threads: _, audio_stream: _, } = self;
// i won't get a window_event without having a window, so unwrap is fine let window = window.as_mut().unwrap(); #[cfg(not(feature = "accesskit"))] let Window { window, render_backend, } = window; #[cfg(feature = "accesskit")] let Window { window, render_backend, adapter, } = window; // don't want window to be mutable let window = window.as_ref();
#[cfg(feature = "accesskit")] adapter.0.process_event(window, &event); // limit the pages and widgets to only push events and not read or pop let event_queue = &mut EventQueue(event_queue);
match event { WindowEvent::CloseRequested => Self::close_requested(event_queue), WindowEvent::Resized(physical_size) => { render_backend.resize(physical_size); window.request_redraw(); } WindowEvent::ScaleFactorChanged { scale_factor: _, inner_size_writer: _, } => { // window_state.resize(**new_inner_size); // due to a version bump in winit i dont know anymore how to handle this event so i just ignore it for know and see if it makes problems in the future // i have yet only received this event on linux wayland, not macos println!("Window Scale Factor Changed"); } WindowEvent::RedrawRequested => { // draw the new frame buffer // TODO: split redraw header and redraw page. As soon as header gets a spectrometer this becomes important header.draw(draw_buffer); ui_pages.draw(draw_buffer); dialog_manager.draw(draw_buffer); #[cfg(feature = "accesskit")] adapter.0.update_if_active(|| { Self::produce_full_tree(ui_pages, header, dialog_manager, adapter.1) }); // notify the windowing system that drawing is done and the new buffer is about to be pushed window.pre_present_notify(); // push the framebuffer into GPU/softbuffer and render it onto the screen render_backend.render(&draw_buffer.framebuffer, event_loop); } WindowEvent::KeyboardInput { device_id: _, event, is_synthetic, } => { if is_synthetic { return; }
if event.state.is_pressed() { if event.logical_key == Key::Named(NamedKey::F5) { self.event_queue .push_back(GlobalEvent::Playback(PlaybackType::Song)); return; } else if event.logical_key == Key::Named(NamedKey::F6) { if modifiers.state().shift_key() { self.event_queue .push_back(GlobalEvent::Playback(PlaybackType::FromOrder)); } else { self.event_queue .push_back(GlobalEvent::Playback(PlaybackType::Pattern)); } return; } else if event.logical_key == Key::Named(NamedKey::F8) { self.event_queue .push_back(GlobalEvent::Playback(PlaybackType::Stop)); return; } } // key_event didn't start or stop the song, so process normally if let Some(dialog) = dialog_manager.active_dialog_mut() { match dialog.process_input(&event, modifiers, event_queue) { DialogResponse::Close => { dialog_manager.close_dialog(); // if i close a pop_up i need to redraw the const part of the page as the pop-up overlapped it probably ui_pages.request_draw_const(); window.request_redraw(); } DialogResponse::RequestRedraw => window.request_redraw(), DialogResponse::None => (), } } else { if event.state.is_pressed() && event.logical_key == Key::Named(NamedKey::Escape) { event_queue.push(GlobalEvent::OpenDialog(dialog::DialogEnum::Main( MainMenu::new(), ))); }
match ui_pages.process_key_event(&self.modifiers, &event, event_queue) { PageResponse::RequestRedraw => window.request_redraw(), PageResponse::None => (), } } } // not sure if i need it just to make sure i always have all current modifiers to be used with keyboard events WindowEvent::ModifiersChanged(new_modifiers) => *modifiers = new_modifiers,
_ => (), }
while let Some(event) = self.event_queue.pop_front() { self.user_event(event_loop, event); } }
/// i may need to add the ability for events to add events to the event queue, but that should be possible fn user_event(&mut self, event_loop: &ActiveEventLoop, event: GlobalEvent) { let event_queue = &mut EventQueue(&mut self.event_queue); match event { GlobalEvent::OpenDialog(dialog) => { self.dialog_manager.open_dialog(dialog); _ = self.try_request_redraw(); } GlobalEvent::Page(c) => match self.ui_pages.process_page_event(c, event_queue) { PageResponse::RequestRedraw => _ = self.try_request_redraw(), PageResponse::None => (), }, GlobalEvent::Header(header_event) => { self.header.process_event(header_event); _ = self.try_request_redraw(); } GlobalEvent::GoToPage(pages_enum) => { self.dialog_manager.close_all(); self.ui_pages.switch_page(pages_enum); _ = self.try_request_redraw(); } GlobalEvent::CloseApp => event_loop.exit(), GlobalEvent::CloseRequested => Self::close_requested(event_queue), GlobalEvent::ConstRedraw => { self.ui_pages.request_draw_const(); _ = self.try_request_redraw(); } GlobalEvent::Playback(playback_type) => { let msg = match playback_type { PlaybackType::Song => Some(ToWorkerMsg::Playback(PlaybackSettings::Order { idx: 0, should_loop: true, })), PlaybackType::Stop => Some(ToWorkerMsg::StopPlayback), PlaybackType::Pattern => { Some(ToWorkerMsg::Playback(self.header.play_current_pattern())) } PlaybackType::FromOrder => { Some(ToWorkerMsg::Playback(self.header.play_current_order())) } PlaybackType::FromCursor => None, };
if let Some(msg) = msg { self.audio_stream .as_mut() .expect( "audio stream should always be active, should still handle this error", ) .2 .try_msg_worker(msg) .expect("buffer full. either increase size or retry somehow") } } #[cfg(feature = "accesskit")] GlobalEvent::Accesskit(window_event) => { use accesskit_winit::WindowEvent; match window_event { WindowEvent::InitialTreeRequested => { // there probably should always be a window // make sure we always respond to this event. I hope it can't be send when there is no window let window = self.window.as_mut().unwrap(); window.adapter.0.update_if_active(|| { Self::produce_full_tree( &self.ui_pages, &self.header, &self.dialog_manager, window.adapter.1, ) }); } WindowEvent::ActionRequested(event) => { match event.target.0 / Self::ID_LEVEL_1 { Self::HEADER_ID => todo!("decide if the header should be interactive"), Self::PAGE_ID => { let event = { let mut event = event; // information about lvl 1 was processed, so remove it event.target.0 -= Self::PAGE_ID * Self::ID_LEVEL_1; event }; match self.ui_pages.process_accesskit_event(event) { PageResponse::RequestRedraw => { self.try_request_redraw().unwrap() } PageResponse::None => (), } } Self::DIALOG_ID => (), // should i ignore invalid IDs? probably not as that would be a bug either in accesskit or my code _ => unreachable!(), } } // i don't have any extra state for accessability so i don't need to cleanup anything WindowEvent::AccessibilityDeactivated => (), } } } }
fn exiting(&mut self, _: &ActiveEventLoop) { if let Some(workers) = self.worker_threads.take() { // wait for all the threads to close workers.close_all(); } if self.audio_stream.is_some() { self.close_audio_stream(); } }}
impl App { pub fn new(proxy: EventLoopProxy<GlobalEvent>) -> Self { Self { window: None, draw_buffer: DrawBuffer::default(), modifiers: Modifiers::default(), ui_pages: AllPages::new(proxy.clone()), dialog_manager: DialogManager::new(), header: Header::default(), event_loop_proxy: proxy, worker_threads: None, audio_stream: None, event_queue: VecDeque::with_capacity(3), } }
// TODO: should this be its own function?? or is there something better fn close_requested(events: &mut EventQueue<'_>) { events.push(GlobalEvent::OpenDialog(DialogEnum::Confirm( ConfirmDialog::new( "Close Torque Tracker?", Some(|| GlobalEvent::CloseApp), None, ), ))); }
/// tries to request a redraw. if there currently is no window this fails fn try_request_redraw(&self) -> Result<(), ()> { if let Some(window) = &self.window { window.window.request_redraw(); Ok(()) } else { Err(()) } }
fn build_window(&mut self, event_loop: &ActiveEventLoop) { self.window.get_or_insert_with(|| { let mut attributes = WindowAttributes::default(); attributes.active = true; attributes.resizable = true; attributes.resize_increments = None; attributes.title = String::from("Torque Tracker"); // for accesskit attributes.visible = false;
let window = Arc::new(event_loop.create_window(attributes).unwrap()); let render_backend = RenderBackend::new(window.clone(), render::palettes::Palette::CAMOUFLAGE);
#[cfg(feature = "accesskit")] let adapter = { accesskit_winit::Adapter::with_event_loop_proxy( event_loop, &window, self.event_loop_proxy.clone(), ) };
window.set_visible(true); let size = render_backend.get_size(); Window { window, render_backend, #[cfg(feature = "accesskit")] adapter: (adapter, create_transform(size)), } }); }
// TODO: make this configurable fn start_audio_stream(&mut self) { use cpal::StreamInstant;
assert!(self.audio_stream.is_none()); let host = cpal::default_host(); let device = host.default_output_device().unwrap(); let default_config = device.default_output_config().unwrap(); let (config, buffer_size) = { let mut config = default_config.config(); let buffer_size = { let default = default_config.buffer_size(); match default { SupportedBufferSize::Unknown => 1024, SupportedBufferSize::Range { min, max } => u32::min(u32::max(1024, *min), *max), } }; config.buffer_size = BufferSize::Fixed(buffer_size); (config, buffer_size) }; let mut guard = SONG_MANAGER.lock_blocking(); let (mut worker, buffer_time, status, stream_send) = guard .get_callback::<cpal::OutputStreamTimestamp>( OutputConfig { buffer_size, // channel_count: NonZero::new(config.channels).unwrap(), sample_rate: NonZero::new(config.sample_rate.0).unwrap(), interpolation: torque_tracker_engine::audio_processing::Interpolation::Linear, }, cpal::OutputStreamTimestamp { callback: cpal::StreamInstant::new(0, 0), playback: cpal::StreamInstant::new(0, 0), }, ); // keep the guard as short as possible to not block the async threads drop(guard); let stream = device .build_output_stream( &config, move |data, info| { if let Some(rendered) = worker.render(data.len(), info.timestamp()) { if config.channels == 1 { data.iter_mut() .zip(rendered.iter().map(|r| r.sum_to_mono())) .for_each(|(out, r)| *out = r); } else { data.chunks_exact_mut(usize::from(config.channels)) .zip(rendered.iter().copied()) .for_each(|(out, r)| { let r: [f32; 2] = r.into(); out.fill(0.); out[0] = r[0]; out[1] = r[1]; }); } } }, |err| eprintln!("audio stream err: {err:?}"), None, ) .unwrap(); // spawn a task to process the audio playback status updates let proxy = self.event_loop_proxy.clone(); let task = EXECUTOR.spawn(async move { let buffer_time = buffer_time; let mut status_recv = status; // maybe also send the timestamp every second or so let mut old_playback = None; let mut old_timestamp = OutputStreamTimestamp { callback: StreamInstant::new(0, 0), playback: StreamInstant::new(0, 0), }; loop { smol::Timer::after(buffer_time).await; let Some(read) = status_recv.try_get() else { // we had a lock for way too long, so now we are behing and have to wait for the writer to finish // writing once. Then locking will succeed again. continue; }; let playback = read.0; let timestamp = read.1; // only react on status changes. could at some point be made more granular if playback != old_playback { old_playback = playback; // println!("playback status: {status:?}"); let pos = playback.map(|s| s.position); proxy .send_event(GlobalEvent::Header(HeaderEvent::SetPlayback(pos))) .unwrap(); let pos = playback.map(|s| (s.position.pattern, s.position.row)); proxy .send_event(GlobalEvent::Page(PageEvent::Pattern( PatternPageEvent::PlaybackPosition(pos), ))) .unwrap(); // does a map flatten. idk why it's called and_then let pos = playback.and_then(|s| s.position.order); proxy .send_event(GlobalEvent::Page(PageEvent::OrderList( OrderListPageEvent::SetPlayback(pos), ))) .unwrap(); }
// also check for changed timestamp if timestamp != old_timestamp { old_timestamp = timestamp; // maybe at some point use the data } } }); self.audio_stream = Some((stream, task, stream_send)); }
fn close_audio_stream(&mut self) { let (stream, task, mut stream_send) = self.audio_stream.take().unwrap(); // stop playback _ = stream_send.try_msg_worker(ToWorkerMsg::StopPlayback); _ = stream_send.try_msg_worker(ToWorkerMsg::StopLiveNote); // kill the task. using `cancel` doesn't make sense because it doesn't finishe anyways drop(task); // lastly kill the audio stream drop(stream); }}
#[cfg(feature = "accesskit")]impl App { // Node ID Layout: // xxx_xxx_xxx_xxx_xxx // In Widget lvl 4 // In Page/Dialog/Header lvl 3 // Which Page/Header/Dialog lvl 2 // Page, Dialog or Header lvl 1 // Root const ROOT_ID: NodeId = NodeId(1_000_000_000_000); pub const ID_LEVEL_1: u64 = 1_000_000_000; pub const ID_LEVEL_2: u64 = 1_000_000; pub const ID_LEVEL_3: u64 = 1_000; pub const ID_LEVEL_4: u64 = 1; // do i need this?
pub const HEADER_ID: u64 = 1; pub const PAGE_ID: u64 = 2; pub const DIALOG_ID: u64 = 3;
fn produce_full_tree( pages: &AllPages, header: &Header, dialogs: &DialogManager, transform: accesskit::Affine, ) -> accesskit::TreeUpdate { use accesskit::TreeUpdate; use accesskit::{Node, Role, Tree};
let tree = Tree { root: Self::ROOT_ID, toolkit_name: Some(String::from("Torque Tracker Custom")), toolkit_version: None, }; let mut root_node = Node::new(Role::Window); let mut nodes = Vec::new(); root_node.set_label("Torque Tracker"); root_node.set_language("English"); let header_id = header.build_tree(&mut nodes); root_node.push_child(header_id); root_node.set_transform(transform);
let resp = pages.build_tree(&mut nodes); let mut focused = resp.selected; root_node.push_child(resp.root);
if let Some(dialog) = dialogs.active_dialog() { let resp = dialog.build_tree(&mut nodes); root_node.push_child(resp.root); focused = resp.selected; }
nodes.push((Self::ROOT_ID, root_node)); TreeUpdate { nodes, tree: Some(tree), focus: focused, } }
#[expect(unused)] fn process_accesskit_event(&mut self, event: accesskit::ActionRequest) {}}
impl Drop for App { fn drop(&mut self) { if self.audio_stream.is_some() { self.close_audio_stream(); } }}
#[cfg(feature = "accesskit")]pub struct AccessResponse { pub root: accesskit::NodeId, pub selected: accesskit::NodeId,}
#[cfg(feature = "accesskit")]fn create_transform(size: winit::dpi::PhysicalSize<u32>) -> accesskit::Affine { accesskit::Affine::scale_non_uniform( size.width as f64 / crate::coordinates::WINDOW_SIZE_CHARS.0 as f64, size.height as f64 / crate::coordinates::WINDOW_SIZE_CHARS.1 as f64, )}
fn main() { let event_loop = winit::event_loop::EventLoop::<GlobalEvent>::with_user_event() .build() .unwrap(); event_loop.set_control_flow(ControlFlow::Wait); // i don't need any raw device events. Keyboard and Mouse coming as window events are enough event_loop.listen_device_events(winit::event_loop::DeviceEvents::Never); let event_loop_proxy = event_loop.create_proxy(); let mut app = App::new(event_loop_proxy); app.header.draw_constant(&mut app.draw_buffer);
event_loop.run_app(&mut app).unwrap();}