this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

editor pt1

alice e3049526 92b3a291

+292 -12
+2
tic80_rust/src/editor/mod.rs
··· 1 + pub mod ui; 2 +
+219
tic80_rust/src/editor/ui.rs
··· 1 + use crate::gfx::framebuffer::Framebuffer; 2 + 3 + #[derive(Copy, Clone, Debug, PartialEq, Eq)] 4 + pub enum Tab { 5 + Code, 6 + Console, 7 + } 8 + 9 + #[derive(Copy, Clone, Debug)] 10 + struct Rect { 11 + x: i32, 12 + y: i32, 13 + w: i32, 14 + h: i32, 15 + } 16 + 17 + impl Rect { 18 + #[allow(clippy::missing_const_for_fn)] 19 + fn contains(&self, px: i32, py: i32) -> bool { 20 + px >= self.x && py >= self.y && px < self.x + self.w && py < self.y + self.h 21 + } 22 + } 23 + 24 + pub struct EditorUi { 25 + pub active: Tab, 26 + scale: f64, 27 + // Hit regions in framebuffer space 28 + tab_code: Rect, 29 + tab_console: Rect, 30 + btn_run: Rect, 31 + btn_stop: Rect, 32 + btn_reset: Rect, 33 + } 34 + 35 + impl EditorUi { 36 + #[must_use] 37 + #[allow(clippy::missing_const_for_fn)] 38 + pub fn new(scale: f64) -> Self { 39 + // Tabs (static for now) 40 + let tab_code = Rect { 41 + x: 4, 42 + y: 2, 43 + w: 40, 44 + h: 10, 45 + }; 46 + let tab_console = Rect { 47 + x: 48, 48 + y: 2, 49 + w: 60, 50 + h: 10, 51 + }; 52 + 53 + // Compute button rectangles from label width (ADV=6) plus padding 54 + let adv: i32 = 6; 55 + let pad_x: i32 = 4; 56 + let h: i32 = 10; 57 + let y: i32 = 2; 58 + let gap: i32 = 4; 59 + let right_margin: i32 = 4; 60 + 61 + let run_w = adv * i32::try_from("RUN".len()).unwrap_or(3) + pad_x * 2; // 26 62 + let stop_w = adv * i32::try_from("STOP".len()).unwrap_or(4) + pad_x * 2; // 32 63 + let reset_w = adv * i32::try_from("RESET".len()).unwrap_or(5) + pad_x * 2; // 38 64 + 65 + let mut right = 240 - right_margin; 66 + let btn_reset = Rect { 67 + x: right - reset_w, 68 + y, 69 + w: reset_w, 70 + h, 71 + }; 72 + right = btn_reset.x - gap; 73 + let btn_stop = Rect { 74 + x: right - stop_w, 75 + y, 76 + w: stop_w, 77 + h, 78 + }; 79 + right = btn_stop.x - gap; 80 + let btn_run = Rect { 81 + x: right - run_w, 82 + y, 83 + w: run_w, 84 + h, 85 + }; 86 + 87 + Self { 88 + active: Tab::Code, 89 + scale, 90 + tab_code, 91 + tab_console, 92 + btn_run, 93 + btn_stop, 94 + btn_reset, 95 + } 96 + } 97 + 98 + #[must_use] 99 + #[allow(clippy::missing_const_for_fn)] 100 + pub fn scale(&self) -> f64 { 101 + self.scale 102 + } 103 + 104 + // Handle a click in framebuffer coordinates 105 + pub fn on_click_fb(&mut self, x: i32, y: i32) { 106 + if self.tab_code.contains(x, y) { 107 + self.active = Tab::Code; 108 + } else if self.tab_console.contains(x, y) { 109 + self.active = Tab::Console; 110 + } else if self.btn_run.contains(x, y) { 111 + // No-op for now 112 + } else if self.btn_stop.contains(x, y) || self.btn_reset.contains(x, y) { 113 + // No-op 114 + } 115 + } 116 + 117 + // Map window coordinates (pixels) to framebuffer space, given integer scale 118 + #[must_use] 119 + pub fn window_to_fb(&self, wx: f64, wy: f64) -> (i32, i32) { 120 + let sx = self.scale; 121 + #[allow(clippy::cast_possible_truncation)] 122 + let x = (wx / sx).floor() as i32; 123 + #[allow(clippy::cast_possible_truncation)] 124 + let y = (wy / sx).floor() as i32; 125 + (x, y) 126 + } 127 + 128 + pub fn draw(&self, fb: &mut Framebuffer) { 129 + // Clear background lightly 130 + fb.cls(0); 131 + // Top bar 132 + fb.rect(0, 0, 240, 12, 5); 133 + 134 + // Tabs 135 + let (code_col, cons_col) = match self.active { 136 + Tab::Code => (12, 8), 137 + Tab::Console => (8, 12), 138 + }; 139 + fb.rect( 140 + self.tab_code.x, 141 + self.tab_code.y, 142 + self.tab_code.w, 143 + self.tab_code.h, 144 + code_col, 145 + ); 146 + fb.rect( 147 + self.tab_console.x, 148 + self.tab_console.y, 149 + self.tab_console.w, 150 + self.tab_console.h, 151 + cons_col, 152 + ); 153 + // Buttons 154 + fb.rect( 155 + self.btn_run.x, 156 + self.btn_run.y, 157 + self.btn_run.w, 158 + self.btn_run.h, 159 + 3, 160 + ); 161 + fb.rect( 162 + self.btn_stop.x, 163 + self.btn_stop.y, 164 + self.btn_stop.w, 165 + self.btn_stop.h, 166 + 9, 167 + ); 168 + fb.rect( 169 + self.btn_reset.x, 170 + self.btn_reset.y, 171 + self.btn_reset.w, 172 + self.btn_reset.h, 173 + 10, 174 + ); 175 + 176 + // Labels (using small scale) 177 + let _ = fb.print_text( 178 + "CODE", 179 + self.tab_code.x + 5, 180 + self.tab_code.y + 2, 181 + 0, 182 + true, 183 + 1, 184 + false, 185 + ); 186 + let _ = fb.print_text( 187 + "CONSOLE", 188 + self.tab_console.x + 3, 189 + self.tab_console.y + 2, 190 + 0, 191 + true, 192 + 1, 193 + false, 194 + ); 195 + // Center button labels 196 + let adv = 6i32; 197 + let run_tx = 198 + self.btn_run.x + (self.btn_run.w - adv * i32::try_from("RUN".len()).unwrap_or(3)) / 2; 199 + let stop_tx = self.btn_stop.x 200 + + (self.btn_stop.w - adv * i32::try_from("STOP".len()).unwrap_or(4)) / 2; 201 + let reset_tx = self.btn_reset.x 202 + + (self.btn_reset.w - adv * i32::try_from("RESET".len()).unwrap_or(5)) / 2; 203 + let _ = fb.print_text("RUN", run_tx, self.btn_run.y + 2, 0, true, 1, false); 204 + let _ = fb.print_text("STOP", stop_tx, self.btn_stop.y + 2, 0, true, 1, false); 205 + let _ = fb.print_text("RESET", reset_tx, self.btn_reset.y + 2, 0, true, 1, false); 206 + 207 + // Active panel background 208 + match self.active { 209 + Tab::Code => { 210 + fb.rect(0, 12, 240, 124, 1); 211 + let _ = fb.print_text("[CODE]", 6, 16, 12, true, 1, false); 212 + } 213 + Tab::Console => { 214 + fb.rect(0, 12, 240, 124, 2); 215 + let _ = fb.print_text("[CONSOLE]", 6, 16, 12, true, 1, false); 216 + } 217 + } 218 + } 219 + }
+4
tic80_rust/src/lib.rs
··· 32 32 pub mod fft; 33 33 pub mod vqt; 34 34 } 35 + 36 + pub mod editor { 37 + pub mod ui; 38 + }
+44 -12
tic80_rust/src/main.rs
··· 42 42 use tic80_rust::audio::capture as audio_cap; 43 43 use tic80_rust::audio::fft::{set_global_fft, FFTState}; 44 44 use tic80_rust::core::memory::Memory; 45 + use tic80_rust::editor::ui::EditorUi; 45 46 use tic80_rust::gfx::framebuffer::{dimensions, Framebuffer}; 46 47 use tic80_rust::script::lua_runner::LuaRunner; 47 48 ··· 82 83 debug_fx: bool, 83 84 quiet: bool, 84 85 help: bool, 86 + editor: bool, 85 87 } 86 88 87 89 fn parse_args() -> Args { ··· 96 98 debug_fx: false, 97 99 quiet: false, 98 100 help: false, 101 + editor: false, 99 102 }; 100 103 while let Some(arg) = args_iter.next() { 101 104 match arg.as_str() { ··· 106 109 "--debug-fft" => out.debug_fft = true, 107 110 "--debug-fx" => out.debug_fx = true, 108 111 "--quiet" => out.quiet = true, 112 + "--editor" => out.editor = true, 109 113 "--audio-device" => { 110 114 if let Some(val) = args_iter.next() { 111 115 out.audio_device = Some(val); ··· 275 279 fn run() -> Result<(), Error> { 276 280 let event_loop = EventLoop::new(); 277 281 let (window, mut pixels) = create_window_and_pixels(&event_loop, 3.0)?; // default integer scaling 282 + let window_scale = 3.0f64; 278 283 279 284 let fb = Rc::new(RefCell::new(Framebuffer::new())); 280 285 let mem = Rc::new(RefCell::new(Memory::new(fb.clone()))); ··· 285 290 print_help(); 286 291 return Ok(()); 287 292 } 293 + if args.help { 294 + print_help(); 295 + return Ok(()); 296 + } 288 297 if args.list_audio { 289 298 print_devices_and_exit(); 290 299 return Ok(()); 291 300 } 292 - let script = load_script(args.script_path.as_ref()); 293 301 tic80_rust::script::lua_runner::set_quiet(args.quiet); 294 - let lua_runner = match LuaRunner::new(fb.clone(), mem.clone(), &script) { 295 - Ok(r) => Some(r), 296 - Err(e) => { 297 - eprintln!("Lua initialization error: {e}"); 298 - None 299 - } 302 + let (lua_runner, mut editor_ui) = if args.editor { 303 + (None, Some(EditorUi::new(window_scale))) 304 + } else { 305 + let script = load_script(args.script_path.as_ref()); 306 + let lr = match LuaRunner::new(fb.clone(), mem.clone(), &script) { 307 + Ok(r) => Some(r), 308 + Err(e) => { 309 + eprintln!("Lua initialization error: {e}"); 310 + None 311 + } 312 + }; 313 + (lr, None) 300 314 }; 301 315 let mut audio_state = init_audio(&args); 302 316 let mut warned_no_tic = false; 317 + let mut last_cursor_fb: Option<(i32, i32)> = None; 303 318 304 319 event_loop.run(move |event, _, control_flow| { 305 320 *control_flow = ControlFlow::Poll; 306 321 match event { 307 322 Event::WindowEvent { event, .. } => match event { 308 323 WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, 324 + WindowEvent::CursorMoved { position, .. } => { 325 + if let Some(ui) = editor_ui.as_ref() { 326 + let (fx, fy) = ui.window_to_fb(position.x, position.y); 327 + last_cursor_fb = Some((fx, fy)); 328 + } 329 + } 330 + WindowEvent::MouseInput { state: ElementState::Pressed, .. } => { 331 + if let (Some((fx, fy)), Some(ui)) = (last_cursor_fb, editor_ui.as_mut()) { 332 + ui.on_click_fb(fx, fy); 333 + } 334 + } 309 335 WindowEvent::KeyboardInput { 310 336 input: 311 337 KeyboardInput { ··· 322 348 }, 323 349 Event::MainEventsCleared => { 324 350 if ticker.should_tick() { 325 - if let Some(r) = &lua_runner { 326 - r.tick(); 327 - } else if !warned_no_tic && !args.quiet { 328 - eprintln!("No TIC() to run; idle"); 329 - warned_no_tic = true; 351 + if editor_ui.is_none() { 352 + if let Some(r) = &lua_runner { 353 + r.tick(); 354 + } else if !warned_no_tic && !args.quiet { 355 + eprintln!("No TIC() to run; idle"); 356 + warned_no_tic = true; 357 + } 330 358 } 331 359 // Simple VU meter from audio ring 332 360 if let Some(a) = audio_state.as_mut() { ··· 437 465 } 438 466 Event::RedrawRequested(_) => { 439 467 let frame = pixels.frame_mut(); 468 + if let Some(ui) = editor_ui.as_ref() { 469 + let mut fbb = fb.borrow_mut(); 470 + ui.draw(&mut fbb); 471 + } 440 472 fb.borrow().blit_to_rgba(frame); 441 473 if let Err(err) = pixels.render() { 442 474 eprintln!("Render error: {err}");
+23
tic80_rust/tests/editor_smoke.rs
··· 1 + use std::cell::RefCell; 2 + use std::rc::Rc; 3 + 4 + use tic80_rust::editor::ui::{EditorUi, Tab}; 5 + use tic80_rust::gfx::framebuffer::Framebuffer; 6 + 7 + #[test] 8 + fn editor_draws_top_bar_and_switches_tabs() { 9 + let mut ui = EditorUi::new(3.0); 10 + let fb = Rc::new(RefCell::new(Framebuffer::new())); 11 + { 12 + let mut fbb = fb.borrow_mut(); 13 + ui.draw(&mut fbb); 14 + // Top bar pixel should be non-zero (colored) 15 + assert!(fbb.pix(1, 1, None).unwrap_or(0) != 0); 16 + } 17 + // Click on CONSOLE tab 18 + ui.on_click_fb(50, 5); 19 + assert_eq!(ui.active, Tab::Console); 20 + // Click back to CODE 21 + ui.on_click_fb(6, 5); 22 + assert_eq!(ui.active, Tab::Code); 23 + }