Skip to content

Commit 13586b8

Browse files
AndrewAltimitAI Agent BotclaudeAI Review AgentAI Pipeline Agent
authored
feat: comprehensive browser rendering improvements (#31)
* feat: major browser rendering improvements across 8 phases Wire in 3 complete but dead-code layout modules (table, float, positioning), fix text measurement, add border styles, overflow clipping, collect <style> blocks and inline style="" attributes from the DOM (previously silently ignored), support @media queries, inherit/initial keywords, and optimize cascade with HashMap lookups. Phase 1: Wire table.rs (1,217 LOC) into block layout Phase 2: Wire float.rs (485 LOC) with FloatContext, clear, clearfix Phase 3: Wire positioning.rs (833 LOC) - already partially connected Phase 4: Fix text width from char-count to bitmap_measure_text() Phase 5: Dashed/dotted/double borders, overflow:hidden clipping Phase 6: inherit/initial keywords, @media query parsing (screen, max-width, min-width) Phase 7: Collect <style> blocks + inline style="" from DOM into cascade (critical fix - these were completely ignored) Phase 8: O(1) inline style lookup via HashMap in cascade Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add mouse scroll wheel support and visual scrollbars for browser and terminal Add InputEvent::MouseWheel variant and SDL2 mouse wheel event mapping. Browser and terminal now respond to scroll wheel input with smooth scrolling and display prominent 6px scrollbar tracks with proportional thumbs on the right edge of their content areas. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: browser phases 9-10 — pseudo-elements, hover/visited, clippy fixes Add ::before/::after pseudo-element support (Phase 9) with CSS content property parsing, pseudo-element selector matching, and inline text box generation in the layout engine. Add :hover, :visited, and :link pseudo-class support (Phase 10) via a new CascadeContext that threads hover node and visited URL state through the entire CSS cascade. BrowserWidget tracks visited URLs and responds to CursorMove events for hover hit-testing against link regions. Also fixes pre-existing clippy warnings (collapsible if blocks, unnecessary cast) and updates all benchmarks for the new style_tree signature. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: connect browser image rendering pipeline (decode, layout, paint) Wire up the full <img> rendering path: decode images from VFS during navigation, use intrinsic dimensions for layout, and upload GPU textures during paint. Previously all pieces existed but were disconnected — images always rendered as broken placeholders. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add image rendering tests (unit + screenshot) 9 unit tests verify the full image pipeline: VFS loading, BMP decoding, intrinsic dimensions, HTML attribute overrides, broken image placeholders, texture creation during paint, and navigation state cleanup. Screenshot test confirms end-to-end rendering with the SDL backend. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve 8 browser layout/paint bugs for correct CSS rendering - Fix parent padding double-counted in child positioning (depth >= 2) - Fix <br> and \n not causing line breaks in inline layout - Fix block centering without margin:auto (add margin_left/right_auto) - Fix inline backgrounds using hardcoded padding instead of CSS values - Fix white-space:nowrap not preventing line wrapping - Apply letter-spacing and word-spacing in text measurement - Add parent-child margin collapsing (top and bottom) - Apply text-indent to first line of inline content Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * perf: optimize browser with time-sliced images, selector index, incremental hover, O(1) LRU Five-phase performance optimization for the browser engine: 1. Time-sliced image loading: replace synchronous fetch-all-images loop with deferred per-frame batches (8ms budget), so page text renders immediately and images stream in. Auto-downscale images exceeding 1M pixels. 2. CSS selector index: bucket rules by rightmost selector (id/class/tag/ universal) for O(1) candidate lookup instead of testing every rule against every element (~10x fewer match attempts). 3. Incremental hover restyle: cache parsed stylesheets, re-cascade only ancestor chains of old/new hover nodes instead of entire DOM. Throttle hover restyles to 20/sec. 4. Reduce allocations: cache image_info_map with dirty flag (no per-layout rebuild). Derive PartialEq on ComputedStyle for change detection. 5. O(1) LRU cache: replace VecDeque O(N) retain() with arena-backed doubly-linked list. Add decoded image eviction with 8MB memory budget. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 13 browser rendering correctness improvements across 5 phases Phase 1 — inline/text: fix emergency word-break letter-spacing, text decoration positioning (underline/strikethrough), CSS width/height on <img> elements with aspect ratio preservation, remove dead paint_line_box. Phase 2 — dimensions: percentage height resolution via containing height, min/max-height properties and clamping, min/max-width constraint enforcement in block width calculation. Phase 3 — CSS cascade: margin-top/bottom auto flags, currentcolor resolution for border colors via resolve_color_or_current(). Phase 4 — UA defaults: add body { margin: 8px } per CSS 2.1 spec. Phase 5 — paint: clamp negative letter-spacing widths to zero, propagate overflow:hidden clip rect to nested children via PaintContext. 13 new tests added, 742 browser tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add CSS custom properties (var()) support for Wikipedia rendering Wikipedia's Codex design system defines 352 custom properties in :root and references them 385 times via var(). This adds full support: - :root pseudo-class matching (matches <html> element) - CssValue::Var variant for unresolved var(--name, fallback) references - Custom property declarations (--*) stored as raw CSS text - custom_properties HashMap on ComputedStyle with CSS inheritance - Two-pass cascade: first builds property map, then resolves var() refs - resolve_css_var() with chained variable and fallback support - var() handling in border/background shorthand expansion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: browser rendering improvements for Google and Wikipedia pages - Fix prefers-color-scheme:dark media query to not match (light mode default) - Change unknown media features to reject instead of match (safer default) - Add border-top/right/bottom/left shorthand expansion - Fix border-style:none forcing border-width to 0 per CSS spec - Add inline element horizontal margin support (links no longer run together) - Add presentational HTML attributes (bgcolor, width, height, align, etc.) - Add <input> element rendering (text inputs and submit buttons) - Add <center> tag support and InlineBlock layout fixes - Add Google and Wikipedia HTML test fixtures with screenshot tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: 8-phase browser rendering quality improvements Add box-sizing: border-box, CSS-styled inputs/buttons, unicode tofu fallback, percentage min/max dimensions, vertical-align, HR border styling, and background-image support. Fixes text centering in submit buttons via bitmap measurement and replaces unrenderable glyphs with ?. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 3 critical browser rendering bugs — whitespace, tables, HR - Preserve inter-element whitespace: collapse_whitespace() no longer strips leading/trailing spaces from text nodes, fixing "a bold and italic" rendering as "aboldanditalic" - Fix table cell content positioning: offset all descendants after build_table_box() repositions cells so content renders at correct table position instead of (0,0) - Fix block-level replaced elements: HR now participates in block flow with full container width instead of getting zero-width inline layout - Add line-boundary whitespace trimming per CSS 2.1 §16.6.1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: 7-phase browser rendering quality improvements (round 3) Implements phases 1-10 of the comprehensive browser improvement plan: - Phase 1: Bold/italic text via faux-bold double-strike and skew rendering - Phase 2: Extended character set (Latin-1, Cyrillic, 50+ Unicode symbols) - Phase 3: Sub-pixel font scaling with Bresenham interpolation - Phase 4: Form element rendering (3D inset inputs, raised buttons, placeholders) - Phase 5: CSS margin collapsing (parent-child, empty blocks, BFC inhibition) - Phase 6: Linear gradients, text-shadow, background-image gradient rendering - Phase 7: Table layout fixes (thead/tbody/tfoot display, cell borders, valign) - Phase 8: text-overflow ellipsis, word-break, overflow-wrap CSS properties - Phase 9: User-agent stylesheet with proper defaults for all HTML elements - Phase 10: Full-page screenshot infrastructure with wiki/news test fixtures Key fixes: thead/tbody/tfoot display changed from table-row to block (was causing all table body content to be invisible), sub-pixel font scaling for accurate text sizes, and proper margin collapsing per CSS 2.1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: 5-phase browser rendering improvements (round 4) Fix button text content (<button>Search</button> now shows "Search" instead of "Button"), add percentage padding/margin resolution against containing block width, add <select> dropdown rendering with selected option label, fix inline-block percentage widths resolving against real available width instead of 10000px, and add Wikipedia real portal test fixture exercising all fixes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address AI review feedback (iteration 1) Automated fix by Claude in response to Gemini/Codex review. Iteration: 1/5 Co-Authored-By: AI Review Agent <noreply@anthropic.com> * fix: address AI review feedback (iteration 2) Fix SelectorIndex mixed selector list bug where rules with both keyed and non-keyed selectors (e.g. `*, .foo`) missed the universal bucket, causing non-keyed elements to miss valid matches. Fix pseudo-element content resolution to respect cascade ordering (specificity, !important, source order) instead of naive last-wins iteration order. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve CI pipeline failures Automated fix by Claude in response to pipeline failures. Failures addressed: - format - lint - test-suite Actions taken: - Ran autoformat (ruff format, cargo fmt) - Fixed remaining lint issues Iteration: 1/5 Co-Authored-By: AI Pipeline Agent <noreply@anthropic.com> * fix: address AI review feedback (iteration 3) - Fix expand_border_side assigning unknown keywords to style instead of color (e.g. currentcolor was misclassified as border-style) - Fix matches_selector_ignoring_pseudo dropping the combinator when pseudo-element compound becomes empty (e.g. `div ::before` matched div itself instead of descendants) - Fix trim_line_boundary_spaces skipping trailing space trim on single-fragment lines due to unnecessary len > 1 guard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: AI Agent Bot <ai-agent@localhost> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: AI Review Agent <ai-review-agent@localhost> Co-authored-by: AI Pipeline Agent <ai-pipeline-agent@localhost>
1 parent 1e29489 commit 13586b8

44 files changed

Lines changed: 10574 additions & 507 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/oasis-app/src/app_state.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ pub struct AppState {
6565
pub radio_manager: RadioManager,
6666
pub radio_source: Option<Box<dyn RadioSource>>,
6767
pub audio_backend: SdlAudioBackend,
68+
pub terminal_scroll_offset: usize,
6869
}
6970

7071
#[cfg(test)]

crates/oasis-app/src/commands.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ mod tests {
430430
radio_manager: RadioManager::new(),
431431
radio_source: None,
432432
audio_backend: SdlAudioBackend::new(),
433+
terminal_scroll_offset: 0,
433434
}
434435
}
435436

crates/oasis-app/src/input.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ pub fn handle_desktop_input(
134134
bw.handle_input(&InputEvent::Backspace, vfs);
135135
}
136136
},
137+
InputEvent::MouseWheel { delta } => {
138+
if state.wm.active_window() == Some("browser")
139+
&& let Some(ref mut bw) = state.browser
140+
{
141+
bw.handle_input(&InputEvent::MouseWheel { delta: *delta }, vfs);
142+
}
143+
},
137144
InputEvent::ButtonPress(btn) => {
138145
if let Some(active_id) = state.wm.active_window().map(|s| s.to_string()) {
139146
if active_id == "browser" {
@@ -380,6 +387,7 @@ pub fn handle_default_input(
380387
InputEvent::ButtonPress(Button::Confirm) if state.mode == Mode::Terminal => {
381388
let line = state.input_buf.clone();
382389
state.input_buf.clear();
390+
state.terminal_scroll_offset = 0;
383391
if !line.is_empty() {
384392
state.output_lines.push(format!("> {line}"));
385393
let pending_skin_swap;
@@ -413,6 +421,24 @@ pub fn handle_default_input(
413421
state.mode = Mode::Dashboard;
414422
},
415423

424+
InputEvent::MouseWheel { delta } if state.mode == Mode::Terminal => {
425+
let len = state.output_lines.len();
426+
let max_visible = terminal_sdi::VISIBLE_OUTPUT_LINES;
427+
if len > max_visible {
428+
let max_offset = len - max_visible;
429+
if *delta < 0 {
430+
// Scroll up (show older lines).
431+
state.terminal_scroll_offset =
432+
(state.terminal_scroll_offset + (-*delta as usize) * 3).min(max_offset);
433+
} else {
434+
// Scroll down (show newer lines).
435+
state.terminal_scroll_offset = state
436+
.terminal_scroll_offset
437+
.saturating_sub(*delta as usize * 3);
438+
}
439+
}
440+
},
441+
416442
_ => {},
417443
}
418444
InputResult::Continue
@@ -554,6 +580,7 @@ mod tests {
554580
radio_manager: RadioManager::new(),
555581
radio_source: None,
556582
audio_backend: SdlAudioBackend::new(),
583+
terminal_scroll_offset: 0,
557584
};
558585
let sdi = SdiRegistry::new();
559586
let vfs = MemoryVfs::new();
@@ -680,7 +707,7 @@ mod tests {
680707
let (mut state, mut sdi, mut vfs) = make_test_state();
681708
state.mode = Mode::Terminal;
682709
// First create terminal objects so set_terminal_visible can hide them.
683-
terminal_sdi::setup_terminal_objects(&mut sdi, &[], "/", "");
710+
terminal_sdi::setup_terminal_objects(&mut sdi, &[], "/", "", 0);
684711
handle_default_input(
685712
&InputEvent::ButtonPress(Button::Cancel),
686713
&mut state,

crates/oasis-app/src/main.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ fn main() -> Result<()> {
170170
ab.init().ok();
171171
ab
172172
},
173+
terminal_scroll_offset: 0,
173174
};
174175

175176
// Load radio stations from VFS.
@@ -349,6 +350,11 @@ fn main() -> Result<()> {
349350
// Update SDI scene graph for the active mode.
350351
render::update_sdi(&mut state, &mut sdi);
351352

353+
// Drive browser image streaming (progressive loading).
354+
if let Some(ref mut bw) = state.browser {
355+
bw.tick(&vfs);
356+
}
357+
352358
// -- Render --
353359
backend.clear(state.bg_color)?;
354360
if state.mode == Mode::Desktop && state.wm.window_count() > 0 {
@@ -374,6 +380,15 @@ fn main() -> Result<()> {
374380
sdi.draw(&mut backend)?;
375381
}
376382

383+
// Paint terminal scrollbar when in terminal mode.
384+
if state.mode == Mode::Terminal {
385+
terminal_sdi::paint_terminal_scrollbar(
386+
&mut backend,
387+
state.output_lines.len(),
388+
state.terminal_scroll_offset,
389+
)?;
390+
}
391+
377392
// Draw transition overlay if active.
378393
if let Some(ref mut trans) = state.active_transition {
379394
trans.draw_overlay(&mut backend)?;

crates/oasis-app/src/render.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub fn update_sdi(state: &mut AppState, sdi: &mut SdiRegistry) {
4747
&state.output_lines,
4848
&state.cwd,
4949
&state.input_buf,
50+
state.terminal_scroll_offset,
5051
);
5152
},
5253
Mode::App => {

0 commit comments

Comments
 (0)