Skip to content

TreeBuilder-based layouts have invisible cursors when text is empty #391

@kekelp

Description

@kekelp

Even when the text is empty, a layout needs to have some proper line metrics, so that for example Cursor::geometry(&layout) can give a cursor of the correct size. RangedBuilder does this correctly, but TreeBuilder just fills the whole LineMetrics with zeroes. This results in an invisible cursor for any TreeBuilder-based text edit box when the text is empty.

This is a minimal example:

use parley::*;

fn main() {
    let mut font_cx = FontContext::new();    
    let mut layout_cx = LayoutContext::new();    
    let style: TextStyle<[u8; 4]> = TextStyle::default();

    println!("RangedBuilder with empty text ");
    test_cursor_geometry_ranged(&mut layout_cx, &mut font_cx, &style, "");
    
    println!();

    println!("TreeBuilder with empty text ");
    test_cursor_geometry(&mut layout_cx, &mut font_cx, &style, "");
}

fn test_cursor_geometry(layout_cx: &mut LayoutContext, font_cx: &mut FontContext, style: &TextStyle<[u8; 4]>, text: &str) {
    let mut builder = layout_cx.tree_builder(font_cx, 1.0, true, style);
    
    builder.push_text(text);
    let (mut layout, _) = builder.build();
    layout.break_all_lines(Some(400.0));
        
    for (i, line) in layout.lines().enumerate() {
        println!("Line {}: metrics = {:?}", i, line.metrics());
    }
    
    let cursor = Cursor::from_byte_index(&layout, 0, Affinity::Downstream);
    let geometry = cursor.geometry(&layout, 1.0);
    println!("Cursor geometry: {:?}", geometry);
}

fn test_cursor_geometry_ranged(layout_cx: &mut LayoutContext, font_cx: &mut FontContext, style: &TextStyle<[u8; 4]>, text: &str) {
    let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, true);
        
    builder.push_default(StyleProperty::FontStack(FontStack::from(&[FontFamily::Named("system-ui".into())] as &[FontFamily])));
    builder.push_default(StyleProperty::FontSize(style.font_size));

    let mut layout = builder.build(text);
    
    layout.break_all_lines(Some(400.0));
        
    for (i, line) in layout.lines().enumerate() {
        println!("Line {}: metrics = {:?}", i, line.metrics());
    }
    
    let cursor = Cursor::from_byte_index(&layout, 0, Affinity::Downstream);
    let geometry = cursor.geometry(&layout, 1.0);
    println!("Cursor geometry: {:?}", geometry);
}

Output:

RangedBuilder with empty text 
Line 0: metrics = LineMetrics { ascent: 17.104, descent: 4.688, leading: 0.0, line_height: 21.792, baseline: 16.0, offset: 0.0, advance: 4.1600003, trailing_whitespace: 4.1600003, min_coord: -1.0, max_coord: 22.0 }
Cursor geometry: Rect { x0: 0.0, y0: -1.0, x1: 1.0, y1: 22.0 }

TreeBuilder with empty text 
Line 0: metrics = LineMetrics { ascent: 0.0, descent: 0.0, leading: 0.0, line_height: 0.0, baseline: 0.0, offset: 0.0, advance: 0.0, trailing_whitespace: 0.0, min_coord: 0.0, max_coord: 0.0 }
Cursor geometry: Rect { x0: 0.0, y0: 0.0, x1: 1.0, y1: 0.0 }

I blindly stumbled into a "fix" that looks like this: in TreeStyleBuilder::finish(), right before returning, do

if styles.is_empty() && self.text.is_empty() {
  styles.push(RangedStyle {
      style: self.tree[0].style.clone(),
      range: 0..0,
  });
}

This makes the style available even when the text is empty. I don't know if this can be a proper solution.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions