///|
/// Line-based scroller - scrolls by actual line count, not item count
///
/// Unlike ItemScroller which assumes fixed item heights,
/// LineScroller calculates actual line heights for each item
///|
/// Line scroller state
pub(all) struct LineScroller {
/// Current scroll offset in lines (0 = top)
offset : @signals.Signal[Int]
/// Auto-scroll to bottom when new content added
auto_scroll : @signals.Signal[Bool]
}
///|
pub fn LineScroller::new() -> LineScroller {
{ offset: @signals.signal(0), auto_scroll: @signals.signal(true) }
}
///|
/// Calculate max line scroll offset
fn line_scroller_max_offset(total_lines : Int, visible_height : Int) -> Int {
if total_lines > visible_height {
total_lines - visible_height
} else {
0
}
}
///|
/// Scroll up by n lines
pub fn LineScroller::up(self : LineScroller, n : Int) -> Bool {
let current = self.offset.get()
if current > 0 {
let new_offset = if current >= n { current - n } else { 0 }
self.offset.set(new_offset)
self.auto_scroll.set(false)
true
} else {
false
}
}
///|
/// Scroll down by n lines
pub fn LineScroller::down(
self : LineScroller,
n : Int,
total_lines : Int,
visible_height : Int,
) -> Bool {
let max = line_scroller_max_offset(total_lines, visible_height)
let current = self.offset.get()
if current < max {
let new_offset = if current + n > max { max } else { current + n }
self.offset.set(new_offset)
if new_offset >= max {
self.auto_scroll.set(true)
}
true
} else {
false
}
}
///|
/// Jump to bottom
pub fn LineScroller::to_bottom(
self : LineScroller,
total_lines : Int,
visible_height : Int,
) -> Unit {
let max = line_scroller_max_offset(total_lines, visible_height)
self.offset.set(max)
self.auto_scroll.set(true)
}
///|
/// Auto-scroll to bottom if enabled
pub fn LineScroller::follow_bottom(
self : LineScroller,
total_lines : Int,
visible_height : Int,
) -> Unit {
if self.auto_scroll.get() {
self.to_bottom(total_lines, visible_height)
}
}
///|
/// Check if can scroll up
pub fn LineScroller::can_scroll_up(self : LineScroller) -> Bool {
self.offset.get() > 0
}
///|
/// Check if can scroll down
pub fn LineScroller::can_scroll_down(
self : LineScroller,
total_lines : Int,
visible_height : Int,
) -> Bool {
self.offset.get() < line_scroller_max_offset(total_lines, visible_height)
}
///|
/// Get visible slice of items based on line offset
/// Returns (items_to_show, skip_lines_in_first_item)
/// heights: array of line heights for each item
/// visible_height: number of lines to show
pub fn[T] LineScroller::slice_with_offset(
self : LineScroller,
items : Array[T],
heights : Array[Int],
visible_height : Int,
) -> (Array[T], Int) {
let offset = self.offset.get()
let result : Array[T] = []
// Find first visible item
let mut line_count = 0
let mut start_idx = 0
let mut skip_lines = 0
// Find which item contains the offset line
for i, h in heights {
if line_count + h > offset {
start_idx = i
skip_lines = offset - line_count
break
}
line_count = line_count + h
start_idx = i + 1
}
// Collect items that fit in visible_height
let mut visible_lines = 0
for i = start_idx
i < items.length() && visible_lines < visible_height
i = i + 1 {
result.push(items[i])
let h = if i < heights.length() { heights[i] } else { 1 }
visible_lines = visible_lines + h
}
(result, skip_lines)
}
///|
/// Simpler slice that returns just the visible items
/// (without partial item handling)
pub fn[T] LineScroller::slice(
self : LineScroller,
items : Array[T],
heights : Array[Int],
visible_height : Int,
) -> Array[T] {
let (result, _) = self.slice_with_offset(items, heights, visible_height)
result
}
///|
/// Calculate line height for text content
/// width: available width for text
/// text: the text content
pub fn calc_text_lines(text : String, width : Int) -> Int {
if width <= 0 {
return 1
}
let mut lines = 1
let mut col = 0
for c in text {
if c == '\n' {
lines = lines + 1
col = 0
} else {
let w = @core.char_display_width(c)
if col + w > width {
lines = lines + 1
col = w
} else {
col = col + w
}
}
}
lines
}
///|
/// Calculate height for a labeled block (icon + label + content)
/// indent: indentation string (e.g. " ")
pub fn calc_labeled_block_lines(
content : String,
width : Int,
indent : String,
) -> Int {
let indent_width = @core.string_display_width(indent)
let content_width = width - indent_width
// 1 line for label + lines for content
1 + calc_text_lines(content, content_width)
}