this repo has no description
0
fork

Configure Feed

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

editor finalyl fixed

alice 6ce29003 d2d8582d

+153 -77
+30 -72
tic80_rust/src/editor/code.rs
··· 257 257 } 258 258 } 259 259 260 - #[allow(clippy::cast_possible_truncation)] 260 + #[allow(clippy::cast_possible_truncation, clippy::too_many_lines)] 261 261 pub fn draw(&mut self, fb: &mut crate::gfx::framebuffer::Framebuffer, area: Area) { 262 262 let gutter_w = 24i32; 263 - let lines_vis = (area.h / 8).max(1) as usize; 263 + // Match TIC-80 editor line pitch: 7px (TIC_FONT_HEIGHT + 1) 264 + let line_pitch = 7i32; 265 + let lines_vis = (area.h / line_pitch).max(1) as usize; 264 266 let cols_vis = ((area.w - gutter_w) / 6).max(1) as usize; 265 267 self.ensure_visible(lines_vis, cols_vis); 266 268 ··· 274 276 break; 275 277 } 276 278 // Gutter (1-based line numbers) 277 - let gutter_y = area.y + i32::try_from(i).unwrap_or(0) * 8; 279 + let gutter_y = area.y + i32::try_from(i).unwrap_or(0) * line_pitch; 278 280 let ln = line_idx + 1; 279 281 let label = format!("{ln:>3}"); 280 282 let _ = fb.print_text(&label, area.x + 2, gutter_y, 6, true, 1, false); ··· 287 289 let start = self.scroll_col.min(line.chars().count()); 288 290 let mut iter = line.chars().skip(start); 289 291 let vis: String = iter.by_ref().take(cols_vis).collect(); 290 - // Selection highlight for this line (with TIC-80-style drop shadow) 291 - if let Some((sel_start, sel_end)) = self.selection_range_idx() { 292 - // Compute selection coverage in columns for this visible segment 293 - let line_char_start = self.rope.line_to_char(line_idx); 294 - let line_char_end = line_char_start + self.line_len(line_idx); 295 - let s = sel_start.max(line_char_start); 296 - let e = sel_end.min(line_char_end); 297 - if e > s { 298 - let a = (s - line_char_start) as i32; 299 - let b = (e - line_char_start) as i32; 300 - let a_vis = (a - self.scroll_col as i32).max(0); 301 - let b_vis = (b - self.scroll_col as i32).max(0); 302 - let from = a_vis.min(cols_vis as i32).max(0); 303 - let to = b_vis.min(cols_vis as i32).max(from); 304 - if to > from { 305 - let sel_x = area.x + gutter_w + from * 6; 306 - let y_top = (gutter_y - 1).max(area.y); 307 - let sel_w = (to - from) * 6; 308 - // Fill (7px tall), like caret box 309 - fb.rect(sel_x, y_top, sel_w, 7, 14); 310 - // Decide whether to draw the right shadow for this row segment. 311 - // Only draw if the next line's selection doesn't extend as far right (outer perimeter). 312 - let mut draw_right_shadow = true; 313 - if line_idx + 1 < self.line_count() { 314 - let next_line_char_start = self.rope.line_to_char(line_idx + 1); 315 - let next_line_char_end = next_line_char_start + self.line_len(line_idx + 1); 316 - // Next line selection coverage 317 - let ns = sel_start.max(next_line_char_start); 318 - let ne = sel_end.min(next_line_char_end); 319 - if ne > ns { 320 - let na = (ns.saturating_sub(next_line_char_start)) as i32; 321 - let nb = (ne.saturating_sub(next_line_char_start)) as i32; 322 - let na_vis = (na - self.scroll_col as i32).max(0); 323 - let nb_vis = (nb - self.scroll_col as i32).max(0); 324 - let nfrom = na_vis.min(cols_vis as i32).max(0); 325 - let nto = nb_vis.min(cols_vis as i32).max(nfrom); 326 - // If next line's right edge is strictly greater than this line's, 327 - // skip right shadow here (it's interior to the overall blob). 328 - // Equal width should draw to produce a continuous vertical edge. 329 - if nto > to { 330 - draw_right_shadow = false; 331 - } 332 - } 333 - } 334 - if draw_right_shadow { 335 - // Right edge: start at the same top as fill and span 8px so adjacent rows abut exactly 336 - fb.rect(sel_x + sel_w, y_top, 1, 8, 0); 337 - } 338 - // Only draw bottom shadow if selection does not continue to next line 339 - let continues_down = sel_end >= line_char_end; 340 - if !continues_down { 341 - fb.rect(sel_x, y_top + 7, sel_w, 1, 0); 342 - } 343 - } 292 + // Draw characters cell-by-cell, applying selection overlays where needed 293 + let line_char_start = self.rope.line_to_char(line_idx); 294 + let sel = self.selection_range_idx(); 295 + for (i_vis, ch) in vis.chars().enumerate() { 296 + let cell_x = area.x + gutter_w + i32::try_from(i_vis).unwrap_or(0) * 6; 297 + let cell_y = gutter_y; 298 + let global_idx = line_char_start + start + i_vis; 299 + let selected = sel.is_some_and(|(s, e)| global_idx >= s && global_idx < e); 300 + if selected { 301 + // Shadow and fill per TIC-80 302 + fb.rect(cell_x, cell_y, 7, 7, 0); 303 + fb.rect(cell_x - 1, cell_y - 1, 7, 7, 14); 304 + // Dark glyph on top 305 + let s = ch.to_string(); 306 + let _ = fb.print_text(&s, cell_x, cell_y, 5, true, 1, false); 307 + } else { 308 + // Normal glyph (no selection overlay) 309 + let s = ch.to_string(); 310 + let _ = fb.print_text(&s, cell_x, cell_y, 12, true, 1, false); 344 311 } 345 312 } 346 - // Monospace rendering for alignment (fixed=true) 347 - let _ = fb.print_text(&vis, area.x + gutter_w, gutter_y, 12, true, 1, false); 348 313 } 349 314 350 315 // Caret (red box aligned to 6x8 cell, with 1px drop shadow; underlying glyph drawn dark) ··· 352 317 let row = i32::try_from(self.caret_line - self.scroll_line).unwrap_or(0); 353 318 let col = i32::try_from(self.caret_col.saturating_sub(self.scroll_col)).unwrap_or(0); 354 319 let cell_x = area.x + gutter_w + col * 6; 355 - let cell_y = area.y + row * 8; 356 - // Box aligned to cell: width 6, height 7 (reserve 1px bottom for shadow) 357 - let fill_x = cell_x; 358 - let fill_y = (cell_y - 1).max(area.y); 359 - let fill_w = 6; 360 - let fill_h = 7; 361 - // Fill: palette 8 (red) 362 - fb.rect(fill_x, fill_y, fill_w, fill_h, 8); 363 - // Shadow (palette 0) to the right and along bottom 364 - fb.rect(fill_x + fill_w, fill_y, 1, fill_h, 0); 365 - fb.rect(fill_x, fill_y + fill_h, fill_w, 1, 0); 320 + let cell_y = area.y + row * line_pitch; 321 + // TIC-80 caret style: drop shadow rect (black) then caret rect (red), both 7x7, offset by 1px 322 + fb.rect(cell_x, cell_y, 7, 7, 0); 323 + fb.rect(cell_x - 1, cell_y - 1, 7, 7, 8); 366 324 367 325 // Draw the underlying glyph in dark color to simulate inversion 368 326 let line_idx = self.caret_line; ··· 377 335 if idx < total { 378 336 let ch = full.chars().nth(idx).unwrap_or(' '); 379 337 let s = ch.to_string(); 380 - // Render in color 0 (dark) monospaced aligned to cell; this simulates inversion 381 - let _ = fb.print_text(&s, cell_x, cell_y, 0, true, 1, false); 338 + // Render in dark grey monospaced aligned to cell; this simulates inversion 339 + let _ = fb.print_text(&s, cell_x, cell_y, 5, true, 1, false); 382 340 } 383 341 } 384 342 }
+26 -2
tic80_rust/src/main.rs
··· 33 33 34 34 use pixels::{Pixels, SurfaceTexture}; 35 35 use winit::dpi::LogicalSize; 36 - use winit::event::{ElementState, Event, KeyboardInput, ModifiersState, VirtualKeyCode, WindowEvent}; 36 + use winit::event::{ 37 + ElementState, Event, KeyboardInput, ModifiersState, VirtualKeyCode, WindowEvent, 38 + }; 37 39 use winit::event_loop::{ControlFlow, EventLoop}; 38 40 use winit::window::{Window, WindowBuilder}; 39 41 ··· 85 87 quiet: bool, 86 88 help: bool, 87 89 editor: bool, 90 + // Editor helpers (headless diagnostics) 91 + editor_demo_select: bool, 88 92 // Screenshot/headless 89 93 headless: bool, 90 94 screenshot_path: Option<PathBuf>, ··· 105 109 quiet: false, 106 110 help: false, 107 111 editor: false, 112 + editor_demo_select: false, 108 113 headless: false, 109 114 screenshot_path: None, 110 115 screenshot_scale: 1, ··· 120 125 "--debug-fx" => out.debug_fx = true, 121 126 "--quiet" => out.quiet = true, 122 127 "--editor" => out.editor = true, 128 + "--editor-demo-select" => out.editor_demo_select = true, 123 129 "--headless" => out.headless = true, 124 130 "--screenshot" => { 125 131 if let Some(val) = args_iter.next() { ··· 284 290 }, 285 291 ); 286 292 println!( 287 - "Usage: {prog} [OPTIONS] [CART.lua]\n\nOptions:\n -h, --help Show this help message and exit\n --quiet Suppress once-only warnings and Lua BOOT()/TIC() error prints\n --editor Launch the editor UI (CODE/CONSOLE)\n --headless Run offscreen without opening a window (for screenshots/CI)\n --screenshot <PATH> Save a screenshot and exit (first frame by default)\n --screenshot-scale <N> Integer scale for screenshot (default 1)\n --screenshot-frame <N> Capture after N frames (windowed/headless)\n --list-audio List input audio devices and exit\n --audio-device <SUBSTR> Select input device by substring match (case-insensitive)\n --audio-disable Disable audio capture and analysis\n --audio-vu Print VU peak dBFS once per second\n --debug-fft Print first 16 FFT bins (smoothed, normalized) ~every 500 ms\n --debug-fx Print per-second FX timings plus ring stats (dp/ovf/underrun/consumed, EMA samples/tick, occupancy)\n\nArguments:\n CART.lua Optional path to a Lua cart; defaults to bundled demo when omitted\n\nNotes:\n- Window: fixed 240x136 internal resolution with integer scaling in a desktop window.\n- Audio: selects nearest supported sample rate to 44100 Hz and logs the choice.\n- Ring stats (with --debug-fx): dp=pushed, ovf=overflows, underrun=no-data, consumed=samples, EMA samples/tick, occupancy.\n\nExamples:\n {prog} --screenshot out.png --screenshot-scale 3\n {prog} --headless --screenshot out.png --screenshot-frame 60\n {prog} --editor\n "); 293 + "Usage: {prog} [OPTIONS] [CART.lua]\n\nOptions:\n -h, --help Show this help message and exit\n --quiet Suppress once-only warnings and Lua BOOT()/TIC() error prints\n --editor Launch the editor UI (CODE/CONSOLE)\n --editor-demo-select In headless editor mode, create a 3-line demo selection before drawing\n --headless Run offscreen without opening a window (for screenshots/CI)\n --screenshot <PATH> Save a screenshot and exit (first frame by default)\n --screenshot-scale <N> Integer scale for screenshot (default 1)\n --screenshot-frame <N> Capture after N frames (windowed/headless)\n --list-audio List input audio devices and exit\n --audio-device <SUBSTR> Select input device by substring match (case-insensitive)\n --audio-disable Disable audio capture and analysis\n --audio-vu Print VU peak dBFS once per second\n --debug-fft Print first 16 FFT bins (smoothed, normalized) ~every 500 ms\n --debug-fx Print per-second FX timings plus ring stats (dp/ovf/underrun/consumed, EMA samples/tick, occupancy)\n\nArguments:\n CART.lua Optional path to a Lua cart; defaults to bundled demo when omitted\n\nNotes:\n- Window: fixed 240x136 internal resolution with integer scaling in a desktop window.\n- Audio: selects nearest supported sample rate to 44100 Hz and logs the choice.\n- Ring stats (with --debug-fx): dp=pushed, ovf=overflows, underrun=no-data, consumed=samples, EMA samples/tick, occupancy.\n\nExamples:\n {prog} --screenshot out.png --screenshot-scale 3\n {prog} --headless --screenshot out.png --screenshot-frame 60\n {prog} --editor\n "); 288 294 } 289 295 290 296 fn load_script(script_path: Option<&PathBuf>) -> String { ··· 701 707 let initial = load_script(args.script_path.as_ref()); 702 708 let ui = EditorUi::new(1.0); 703 709 let mut code = CodeBuffer::from_text(&initial); 710 + if args.editor_demo_select { 711 + apply_demo_selection(&mut code); 712 + } 704 713 { 705 714 let mut fbb = fb.borrow_mut(); 706 715 ui.draw(&mut fbb); ··· 727 736 fb.borrow().blit_to_rgba(&mut rgba); 728 737 save_screenshot_from_rgba(args, &rgba)?; 729 738 Ok(()) 739 + } 740 + 741 + // Create a 3-line demo selection with ragged edges to inspect drop shadows. 742 + fn apply_demo_selection(cb: &mut CodeBuffer) { 743 + // Choose lines 6..=8 (0-based) which are likely to exist in default carts. 744 + let l0 = 6usize; 745 + let l2 = 8usize; 746 + // Start mid-line, end a few chars into line 8 to create overhangs. 747 + let c0 = (cb.line_len(l0) / 2).max(2); 748 + let c2 = cb.line_len(l2).min(8); 749 + cb.caret_line = l0; 750 + cb.caret_col = c0; 751 + cb.start_selection(); 752 + cb.caret_line = l2; 753 + cb.caret_col = c2; 730 754 } 731 755 732 756 // -- Clipboard helpers -------------------------------------------------------------------------
+8 -3
tic80_rust/tests/editor_selection_align_tests.rs
··· 14 14 cb.caret_col = 1; 15 15 cb.start_selection(); 16 16 cb.caret_col = 3; // selection [1,3) 17 - // Also place caret for box at col 3 17 + // Also place caret for box at col 3 18 18 let fb = Rc::new(RefCell::new(Framebuffer::new())); 19 19 let mut fbb = fb.borrow_mut(); 20 - let area = Area { x: 0, y: 12, w: 240, h: 124 }; 20 + let area = Area { 21 + x: 0, 22 + y: 12, 23 + w: 240, 24 + h: 124, 25 + }; 21 26 cb.draw(&mut fbb, area); 22 27 23 28 // Expected coordinates ··· 25 30 let row = 0i32; // first line 26 31 let gutter_y = area.y + row * 8; 27 32 let caret_fill_top = (gutter_y - 1).max(area.y); // caret fills 7px starting 1px above baseline, clipped to area 28 - // Selection starts at col 1 (from) over a space (no glyph ink) 33 + // Selection starts at col 1 (from) over a space (no glyph ink) 29 34 let sel_x = area.x + gutter_w + 6; 30 35 let sel_y = caret_fill_top; 31 36
+89
tic80_rust/tests/editor_selection_shadow_tests.rs
··· 1 + use std::cell::RefCell; 2 + use std::rc::Rc; 3 + 4 + use tic80_rust::editor::code::{Area, CodeBuffer}; 5 + use tic80_rust::gfx::framebuffer::Framebuffer; 6 + 7 + // Helper to sample a pixel color from the framebuffer (palette index) 8 + fn px(fb: &mut Framebuffer, x: i32, y: i32) -> u8 { 9 + fb.pix(x, y, None).unwrap_or(0) 10 + } 11 + 12 + #[test] 13 + fn selection_no_seam_between_lines() { 14 + // Three short lines; select across all so middle seam is exercised. 15 + let text = "abc\nabc\nabc\n"; 16 + let mut cb = CodeBuffer::from_text(text); 17 + 18 + // Create selection from start of first line to middle of last line 19 + cb.caret_line = 0; 20 + cb.caret_col = 0; 21 + cb.start_selection(); 22 + cb.caret_line = 2; 23 + cb.caret_col = 2; // up to 'b' 24 + 25 + let fb_rc = Rc::new(RefCell::new(Framebuffer::new())); 26 + let mut fb = fb_rc.borrow_mut(); 27 + let area = Area { 28 + x: 0, 29 + y: 12, 30 + w: 240, 31 + h: 40, 32 + }; 33 + cb.draw(&mut fb, area); 34 + 35 + // Pick column 1 (the 'b') well inside selection run 36 + let gutter_w = 24i32; 37 + let col_x = gutter_w + 6 + 2; // inside col #1 38 + 39 + // With TIC-80 logic there should be NO black seam between consecutive lines. 40 + // Our line pitch should be 7; seam of row0 is at y0 + 6 and is covered by row1 fill. 41 + let row0_y = area.y; 42 + let seam_y = row0_y + 6; // seam between row0 and row1 43 + let c = px(&mut fb, col_x, seam_y); 44 + assert_ne!( 45 + c, 0, 46 + "seam must not be black (should be covered by selection fill)" 47 + ); 48 + } 49 + 50 + #[test] 51 + fn selection_right_edge_shadow_height_is_7() { 52 + let text = "abc def\n"; // ensure next char is space so it doesn't overwrite the right-edge shadow 53 + let mut cb = CodeBuffer::from_text(text); 54 + // Select first 3 columns on a single line, keep caret away from right edge 55 + cb.caret_line = 0; 56 + cb.caret_col = 3; 57 + cb.start_selection(); // anchor at (0,3) 58 + cb.caret_line = 0; 59 + cb.caret_col = 0; // caret at (0,0): selection is (0,3) 60 + 61 + let fb_rc = Rc::new(RefCell::new(Framebuffer::new())); 62 + let mut fb = fb_rc.borrow_mut(); 63 + let area = Area { 64 + x: 0, 65 + y: 12, 66 + w: 240, 67 + h: 20, 68 + }; 69 + cb.draw(&mut fb, area); 70 + 71 + let gutter_w = 24i32; 72 + let right_edge_x = gutter_w + 3 * 6 - 1; // vertical shadow at right edge of col2 73 + let base_y = area.y - 1; // selection fill starts at y-1; shadow spans 7 px down from y 74 + 75 + let mut black_count = 0; 76 + let mut vals = [0u8; 7]; 77 + for dy in 0..7 { 78 + let c = px(&mut fb, right_edge_x + 1, base_y + 1 + dy); 79 + vals[dy as usize] = c; 80 + if c == 0 { 81 + black_count += 1; 82 + } 83 + } 84 + eprintln!("right edge column vals={:?}", vals); 85 + assert_eq!( 86 + black_count, 7, 87 + "right-edge shadow must be exactly 7 px tall" 88 + ); 89 + }