use super::*;
use crate::widget::BackbufferBand;
pub(super) fn diff_line_range(
old: &[WrappedLine],
new: &[WrappedLine],
) -> Option<(usize, usize, bool)> {
let count_changed = old.len() != new.len();
let min_len = old.len().min(new.len());
let mut first = 0;
while first < min_len && old[first].text == new[first].text {
first += 1;
}
if !count_changed && first == new.len() {
return None; }
let mut back = 0;
while back < min_len - first
&& old[old.len() - 1 - back].text == new[new.len() - 1 - back].text
{
back += 1;
}
let last = new.len().saturating_sub(1).saturating_sub(back);
Some((first, last.max(first), count_changed))
}
pub(crate) const BAND_OVERSCAN_FRACTION: f64 = 0.5;
#[derive(Clone, Copy, Default, PartialEq)]
pub(crate) struct BandState {
pub active: bool,
pub anchor: f64,
pub over_top: f64,
pub over_bottom: f64,
}
pub(crate) fn plan_overscan(anchor: f64, max_scroll: f64, margin: f64) -> (f64, f64) {
let margin = margin.max(0.0);
let over_top = margin.min(anchor.max(0.0));
let over_bottom = margin.min((max_scroll - anchor).max(0.0));
(over_top, over_bottom)
}
pub(crate) fn offset_in_band(offset: f64, anchor: f64, over_top: f64, over_bottom: f64) -> bool {
const EPS: f64 = 1e-6;
offset >= anchor - over_top - EPS && offset <= anchor + over_bottom + EPS
}
impl TextArea {
pub(crate) fn recompute_band(&mut self) {
let max_scroll = self.max_scroll_y();
if max_scroll <= 0.0 || self.cached_line_h <= 0.0 {
self.band.active = false;
return;
}
let vp = self.inner_height();
let margin = vp * BAND_OVERSCAN_FRACTION;
let live = self.vbar.offset;
let in_band = self.band.active
&& self.band.anchor <= max_scroll
&& offset_in_band(live, self.band.anchor, self.band.over_top, self.band.over_bottom);
if !in_band {
let anchor = live.clamp(0.0, max_scroll);
let (over_top, over_bottom) = plan_overscan(anchor, max_scroll, margin);
self.band = BandState {
active: true,
anchor,
over_top,
over_bottom,
};
}
}
pub(crate) fn backbuffer_band_impl(&self) -> Option<BackbufferBand> {
if !self.band.active {
return None;
}
Some(BackbufferBand {
overscan_top: self.band.over_top,
overscan_bottom: self.band.over_bottom,
blit_dy: self.vbar.offset - self.band.anchor,
dirty_strip_y: self
.render_dirty_lines
.get()
.map(|(first, last)| self.dirty_strip_y(first, last)),
})
}
pub(crate) fn band_clip_y(&self) -> (f64, f64) {
let lo = self.padding;
let hi = (self.bounds.height - self.padding).max(lo);
if self.band.active {
(lo - self.band.over_bottom, hi + self.band.over_top)
} else {
(lo, hi)
}
}
pub fn debug_raster_count(&self) -> u64 {
self.raster_count.get()
}
pub fn debug_strip_raster_count(&self) -> u64 {
self.strip_raster_count.get()
}
pub(crate) fn plan_dirty_strip(&mut self) {
self.render_dirty_lines.set(None);
if !self.band.active {
return;
}
let Some((first, last, count_changed)) = self.wrap_change else {
return;
};
let Some(prev) = self.last_sig.as_ref() else {
return;
};
let (now_no_sel, _) = {
let st = self.edit.borrow();
(st.cursor == st.anchor, ())
};
let stable = now_no_sel
&& prev.cursor == prev.anchor
&& prev.band_active
&& prev.band_anchor_bits == self.band.anchor.to_bits()
&& prev.band_over_top_bits == self.band.over_top.to_bits()
&& prev.band_over_bottom_bits == self.band.over_bottom.to_bits()
&& prev.w_bits == self.bounds.width.to_bits()
&& prev.h_bits == self.bounds.height.to_bits()
&& prev.h_align == self.resolved_h_align()
&& prev.v_align == self.resolved_v_align()
&& prev.font_size_bits == self.font_size.to_bits();
if !stable || self.cached_lines.is_empty() {
return;
}
let n = self.cached_lines.len();
let end = if count_changed { n - 1 } else { last.min(n - 1) };
let first = first.min(n - 1);
self.render_dirty_lines.set(Some((first, end.max(first))));
}
pub(crate) fn dirty_strip_y(&self, first: usize, last: usize) -> (f64, f64) {
let (clip_lo, clip_hi) = self.band_clip_y();
let hi = self.line_top_y(first).min(clip_hi);
let lo = (self.line_top_y(last) - self.cached_line_h).max(clip_lo);
(lo, hi.max(lo))
}
pub(crate) fn paint_border(&self, ctx: &mut dyn DrawCtx, v: &crate::theme::Visuals) {
let w = self.bounds.width;
let h = self.bounds.height;
let border = if self.focused {
v.accent
} else if self.hovered {
v.widget_stroke_active
} else {
v.widget_stroke
};
let line_width = if self.focused { 2.0 } else { 1.0 };
ctx.set_stroke_color(border);
ctx.set_line_width(line_width);
ctx.begin_path();
let inset = line_width * 0.5;
ctx.rounded_rect(
inset,
inset,
(w - line_width).max(0.0),
(h - line_width).max(0.0),
4.0,
);
ctx.stroke();
}
pub(crate) fn paint_band_frame(&self, ctx: &mut dyn DrawCtx, v: &crate::theme::Visuals) {
let w = self.bounds.width;
let h = self.bounds.height;
let inner_w = (w - self.padding * 2.0).max(0.0);
let inner_h = (h - self.padding * 2.0).max(0.0);
ctx.set_fill_color(v.widget_bg);
ctx.set_fill_rule(crate::draw_ctx::FillRule::EvenOdd);
ctx.begin_path();
ctx.rounded_rect(0.0, 0.0, w, h, 4.0);
ctx.rect(self.padding, self.padding, inner_w, inner_h);
ctx.fill();
ctx.set_fill_rule(crate::draw_ctx::FillRule::NonZero);
self.paint_border(ctx, v);
}
}
#[cfg(test)]
mod tests {
use super::{offset_in_band, plan_overscan};
#[test]
fn overscan_is_symmetric_in_the_interior() {
let (top, bottom) = plan_overscan(1000.0, 2000.0, 400.0);
assert_eq!(top, 400.0);
assert_eq!(bottom, 400.0);
}
#[test]
fn overscan_clamps_to_available_content_at_the_top() {
let (top, bottom) = plan_overscan(100.0, 2000.0, 400.0);
assert_eq!(top, 100.0);
assert_eq!(bottom, 400.0);
}
#[test]
fn overscan_clamps_to_available_content_at_the_bottom() {
let (top, bottom) = plan_overscan(1950.0, 2000.0, 400.0);
assert_eq!(top, 400.0);
assert_eq!(bottom, 50.0);
}
#[test]
fn in_band_covers_anchor_plus_minus_overscan() {
let (anchor, over_top, over_bottom) = (1000.0, 400.0, 400.0);
assert!(offset_in_band(1000.0, anchor, over_top, over_bottom));
assert!(offset_in_band(600.0, anchor, over_top, over_bottom)); assert!(offset_in_band(1400.0, anchor, over_top, over_bottom)); assert!(!offset_in_band(599.0, anchor, over_top, over_bottom));
assert!(!offset_in_band(1401.0, anchor, over_top, over_bottom));
}
#[test]
fn planned_band_never_extends_past_scroll_limits() {
let max_scroll = 2000.0;
let margin = 700.0;
for &anchor in &[0.0, 5.0, 350.0, 1000.0, 1990.0, 2000.0] {
let (top, bottom) = plan_overscan(anchor, max_scroll, margin);
assert!(anchor - top >= -1e-9, "top edge below 0 at anchor {anchor}");
assert!(
anchor + bottom <= max_scroll + 1e-9,
"bottom edge past max at anchor {anchor}"
);
}
}
}