///|
/// Render current content to ANSI text (TUI mode)
fn Browser::render_text(self : Browser) -> String {
if self.html_content.length() == 0 {
return ""
}
let rt0 = perf_now()
let ctx = @browser_helpers.create_render_context(
self.viewport_width,
self.viewport_height,
self.dark_mode,
)
if self.last_color_scheme_dark != self.dark_mode {
self.clear_render_cache()
}
let mut force_full = false
// Build or reuse cached render node + layout tree
let render_node = match (self.render_node, self.layout_tree) {
(Some(node), Some(_tree)) => node
_ => {
force_full = true
let rn0 = perf_now()
let n = match self.parsed_doc {
Some(doc) => self.render_node_from_document(doc, ctx, self.dark_mode)
None =>
@renderer.render_to_node_with_external_css(
self.html_content,
ctx,
self.external_css,
)
}
let rn1 = perf_now()
let tree = self.build_dynamic_layout_tree(
n,
self.viewport_width.to_double(),
self.viewport_height.to_double(),
)
let rn2 = perf_now()
perf_log(
"[perf:build] css_cascade+node=\{(rn1 - rn0).to_int()}ms layout_tree_init=\{(rn2 - rn1).to_int()}ms",
)
self.render_node = Some(n)
self.layout_tree = Some(tree)
n
}
}
let rt1 = perf_now()
let (layout, dirty_rects) = match self.layout_tree {
Some(tree) => {
let l = tree.compute_incremental()
let rects = tree.collect_dirty_rects()
tree.mark_all_layouts_seen()
self.layout_tree = Some(tree)
(l, rects)
}
None => {
force_full = true
let tree = self.build_dynamic_layout_tree(
render_node,
self.viewport_width.to_double(),
self.viewport_height.to_double(),
)
let l = tree.compute_incremental()
let rects = tree.collect_dirty_rects()
tree.mark_all_layouts_seen()
self.layout_tree = Some(tree)
(l, rects)
}
}
let rt2 = perf_now()
// Update content_height from layout result (avoids duplicate layout pass)
if self.content_height == 0 {
self.content_height = @tui.get_content_extent(layout).to_int()
}
// Convert pixel dimensions to character dimensions
// Buffer width = viewport / 8 (wider than terminal for proper CSS layout)
// Output is clipped to terminal_cols at ANSI rendering stage
let char_width = self.viewport_width / 8
let char_height = self.viewport_height / 16
let char_scroll_y = self.scroll_y / 16
// Get focused element name for :focus styling
let focused_element_name = self.get_focused_element_name()
// Get focused link URL for status bar
let focused_link_url = self.get_focused_link_url()
let render_key = self.current_url +
"|" +
self.scroll_y.to_string() +
"|" +
self.focused_link.to_string() +
"|" +
focused_element_name.unwrap_or("") +
"|" +
focused_link_url.unwrap_or("") +
"|" +
self.hint_mode.to_string() +
"|" +
self.hint_input +
"|" +
self.selection_mode.to_string() +
"|" +
self.dark_mode.to_string() +
"|" +
self.viewport_width.to_string() +
"|" +
self.viewport_height.to_string()
if render_key != self.last_render_key {
force_full = true
}
// Use hint mode rendering if in hint mode
let buffer_result = if self.hint_mode {
@tui.render_to_buffer_with_hints(
render_node,
layout,
char_width,
char_height,
char_scroll_y,
self.hints,
self.hint_input,
focused_element_name~,
)
} else {
@tui.render_to_buffer_with_status(
render_node,
layout,
char_width,
char_height,
char_scroll_y,
self.current_url,
self.get_visible_focusable_count(),
self.get_visible_focus_index(),
focused_element_name~,
focused_link_url~,
selection_mode=self.selection_mode,
dark_mode=self.dark_mode,
total_content_height=self.content_height,
show_scrollbar=true,
element_scroll_positions=self.get_element_scroll_positions(),
)
}
let rt3 = perf_now()
// Store link regions for click hit testing
self.link_regions = buffer_result.link_regions
self.hit_regions = buffer_result.hit_regions
self.image_regions = buffer_result.image_regions
// Initialize scroll states for scrollable elements
self.init_scrollable_elements(buffer_result.scrollable_elements)
let plain_links = self.build_plain_links(buffer_result.link_regions)
// Choose buffer-to-output based on no_color mode
// Clip output to terminal_cols (buffer may be wider than terminal)
let max_cols = self.terminal_cols
let ansi = match self.prev_buffer {
Some(prev) =>
if force_full {
if self.no_color {
@tui.clear_screen() +
@tui.buffer_to_plain_with_links(buffer_result.buffer, plain_links)
} else {
@tui.clear_screen() +
@tui.buffer_to_ansi(buffer_result.buffer, max_cols~)
}
} else if dirty_rects.length() > 0 && !self.no_color {
// Diff rendering only for color mode (plain mode always does full redraw)
let content_height = if self.hint_mode {
char_height - 1
} else {
char_height - 1
}
let row_offset = if self.hint_mode { 0 } else { 1 }
let dirty_cells = @tui.dirty_rects_to_cells(
dirty_rects,
self.scroll_y,
char_width,
content_height,
row_offset,
)
@tui.buffer_diff_to_ansi_rects(
buffer_result.buffer,
prev,
dirty_cells,
max_cols~,
)
} else if self.no_color {
@tui.clear_screen() +
@tui.buffer_to_plain_with_links(buffer_result.buffer, plain_links)
} else {
""
}
None =>
if self.no_color {
@tui.clear_screen() +
@tui.buffer_to_plain_with_links(buffer_result.buffer, plain_links)
} else {
@tui.clear_screen() +
@tui.buffer_to_ansi(buffer_result.buffer, max_cols~)
}
}
// Append kitty graphics commands for image regions
let kitty_overlay = self.build_kitty_image_overlay()
let final_output = if kitty_overlay.length() > 0 {
ansi + kitty_overlay
} else {
ansi
}
let rt4 = perf_now()
self.prev_buffer = Some(buffer_result.buffer)
self.last_render_key = render_key
self.last_color_scheme_dark = self.dark_mode
perf_log(
"[perf:render] node+layout_build=\{(rt1 - rt0).to_int()}ms layout_compute=\{(rt2 - rt1).to_int()}ms tui_buffer=\{(rt3 - rt2).to_int()}ms ansi=\{(rt4 - rt3).to_int()}ms total=\{(rt4 - rt0).to_int()}ms",
)
final_output
}
///|
fn Browser::build_plain_links(
self : Browser,
link_regions : Array[@tui.LinkRegion],
) -> Array[@tui.PlainLink] {
let plain_links : Array[@tui.PlainLink] = []
if !self.no_color {
return plain_links
}
for region in link_regions {
match self.get_link_href_for_region(region) {
Some(href) => {
let url = resolve_url(self.current_url, href)
if url.length() > 0 {
plain_links.push({
col: region.col,
row: region.row,
width: region.width,
url,
})
}
}
None => ()
}
}
plain_links
}
///|
pub fn Browser::render_text_full_page(self : Browser) -> String {
if self.html_content.length() == 0 {
return ""
}
let ctx = @browser_helpers.create_render_context(
self.viewport_width,
self.viewport_height,
self.dark_mode,
)
if self.last_color_scheme_dark != self.dark_mode {
self.clear_render_cache()
}
let render_node = match (self.render_node, self.layout_tree) {
(Some(node), Some(_tree)) => node
_ => {
let n = match self.parsed_doc {
Some(doc) => self.render_node_from_document(doc, ctx, self.dark_mode)
None =>
@renderer.render_to_node_with_external_css(
self.html_content,
ctx,
self.external_css,
)
}
let tree = @layout.LayoutTree::from_node(
n,
self.viewport_width.to_double(),
self.viewport_height.to_double(),
)
self.render_node = Some(n)
self.layout_tree = Some(tree)
n
}
}
let layout = match self.layout_tree {
Some(tree) => {
let l = tree.compute_incremental()
tree.mark_all_layouts_seen()
self.layout_tree = Some(tree)
l
}
None => {
let tree = @layout.LayoutTree::from_node(
render_node,
self.viewport_width.to_double(),
self.viewport_height.to_double(),
)
let l = tree.compute_incremental()
tree.mark_all_layouts_seen()
self.layout_tree = Some(tree)
l
}
}
if self.content_height == 0 {
self.content_height = @tui.get_content_extent(layout).to_int()
}
let char_width = self.viewport_width / 8
let content_height = @tui.get_content_height_chars(layout).max(1)
let focused_element_name = self.get_focused_element_name()
let buffer_result = @tui.render_to_buffer(
render_node,
layout,
char_width,
content_height,
0,
focused_element_name~,
)
self.link_regions = buffer_result.link_regions
self.hit_regions = buffer_result.hit_regions
self.image_regions = buffer_result.image_regions
self.init_scrollable_elements(buffer_result.scrollable_elements)
self.prev_buffer = None
self.last_render_key = ""
self.last_color_scheme_dark = self.dark_mode
let plain_links = self.build_plain_links(buffer_result.link_regions)
if self.no_color {
@tui.buffer_to_plain_with_links(buffer_result.buffer, plain_links)
} else {
@tui.buffer_to_ansi(buffer_result.buffer, max_cols=self.terminal_cols)
}
}