cbf-compositor 0.1.0-alpha.7

Scene-based browser surface compositor for CBF applications.
Documentation
use cbf::data::ime::{ImeBoundsUpdate, ImeCompositionBounds, ImeRect, TextSelectionBounds};
use objc2_core_foundation::{CGPoint, CGRect, CGSize};
use objc2_foundation::NSRange;

pub(crate) fn candidate_rect_for_slot(
    range: NSRange,
    slot_bounds: CGRect,
    ime_bounds: Option<&ImeBoundsUpdate>,
) -> Option<CGRect> {
    let bounds = ime_bounds?;

    if let Some(composition) = bounds.composition.as_ref()
        && let Some(rect) = rect_for_composition_range(range, composition)
    {
        return Some(offset_rect(
            flip_rect_in_layer(rect, slot_bounds.size.height),
            slot_bounds.origin.x,
            slot_bounds.origin.y,
        ));
    }

    bounds.selection.as_ref().map(|selection| {
        offset_rect(
            flip_rect_in_layer(rect_from_selection(selection), slot_bounds.size.height),
            slot_bounds.origin.x,
            slot_bounds.origin.y,
        )
    })
}

fn rect_for_composition_range(
    range: NSRange,
    composition: &ImeCompositionBounds,
) -> Option<CGRect> {
    if composition.range_start < 0 || composition.range_end < composition.range_start {
        return None;
    }
    if composition.character_bounds.is_empty() {
        return None;
    }
    if range.location == usize::MAX {
        return None;
    }

    let start = range.location.min(i32::MAX as usize) as i32;
    let end = range.end().min(i32::MAX as usize).max(range.location) as i32;

    if start < composition.range_start || end > composition.range_end {
        return None;
    }

    let local_start = (start - composition.range_start) as usize;
    if local_start >= composition.character_bounds.len() {
        return None;
    }

    if range.length == 0 {
        return Some(rect_from_ime(&composition.character_bounds[local_start]));
    }

    let local_end = (end - composition.range_start) as usize;
    let clamped_end = local_end.min(composition.character_bounds.len());
    if clamped_end <= local_start {
        return Some(rect_from_ime(&composition.character_bounds[local_start]));
    }

    let mut rect = rect_from_ime(&composition.character_bounds[local_start]);
    for bounds in &composition.character_bounds[local_start + 1..clamped_end] {
        rect = union_rect(rect, rect_from_ime(bounds));
    }

    Some(rect)
}

fn rect_from_selection(selection: &TextSelectionBounds) -> CGRect {
    rect_from_ime(&selection.caret_rect)
}

fn rect_from_ime(rect: &ImeRect) -> CGRect {
    CGRect::new(
        CGPoint::new(rect.x as f64, rect.y as f64),
        CGSize::new(rect.width as f64, rect.height as f64),
    )
}

fn union_rect(a: CGRect, b: CGRect) -> CGRect {
    let min_x = a.origin.x.min(b.origin.x);
    let min_y = a.origin.y.min(b.origin.y);
    let max_x = (a.origin.x + a.size.width).max(b.origin.x + b.size.width);
    let max_y = (a.origin.y + a.size.height).max(b.origin.y + b.size.height);

    CGRect::new(
        CGPoint::new(min_x, min_y),
        CGSize::new((max_x - min_x).max(0.0), (max_y - min_y).max(0.0)),
    )
}

fn offset_rect(rect: CGRect, offset_x: f64, offset_y: f64) -> CGRect {
    CGRect::new(
        CGPoint::new(rect.origin.x + offset_x, rect.origin.y + offset_y),
        rect.size,
    )
}

fn flip_rect_in_layer(rect: CGRect, layer_height: f64) -> CGRect {
    let flipped_y = (layer_height - (rect.origin.y + rect.size.height)).max(0.0);
    CGRect::new(CGPoint::new(rect.origin.x, flipped_y), rect.size)
}

#[cfg(test)]
mod tests {
    use cbf::data::ime::{ImeBoundsUpdate, ImeCompositionBounds, ImeRect};
    use objc2_core_foundation::{CGPoint, CGRect, CGSize};
    use objc2_foundation::NSRange;

    use super::candidate_rect_for_slot;

    #[test]
    fn candidate_rect_offsets_into_slot_space() {
        let rect = candidate_rect_for_slot(
            NSRange::new(0, 1),
            CGRect::new(CGPoint::new(20.0, 30.0), CGSize::new(100.0, 50.0)),
            Some(&ImeBoundsUpdate {
                composition: Some(ImeCompositionBounds {
                    range_start: 0,
                    range_end: 1,
                    character_bounds: vec![ImeRect {
                        x: 1,
                        y: 2,
                        width: 3,
                        height: 4,
                    }],
                }),
                selection: None,
            }),
        )
        .unwrap();

        assert_eq!(rect.origin.x, 21.0);
        assert_eq!(rect.origin.y, 74.0);
        assert_eq!(rect.size.width, 3.0);
        assert_eq!(rect.size.height, 4.0);
    }
}