Implement common renderer and scrolling logic
This commit is contained in:
parent
bb4d0fe047
commit
a18ee8e7c0
2 changed files with 349 additions and 0 deletions
|
|
@ -1 +1,2 @@
|
|||
mod blocks;
|
||||
mod renderer;
|
||||
|
|
|
|||
348
src/ui/chat2/renderer.rs
Normal file
348
src/ui/chat2/renderer.rs
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use toss::Size;
|
||||
|
||||
use super::blocks::{Blocks, Range};
|
||||
|
||||
#[async_trait]
|
||||
pub trait Renderer<Id> {
|
||||
type Error;
|
||||
|
||||
fn size(&self) -> Size;
|
||||
fn scrolloff(&self) -> i32;
|
||||
|
||||
fn blocks(&self) -> &Blocks<Id>;
|
||||
fn blocks_mut(&mut self) -> &mut Blocks<Id>;
|
||||
fn into_blocks(self) -> Blocks<Id>;
|
||||
|
||||
async fn expand_top(&mut self) -> Result<(), Self::Error>;
|
||||
async fn expand_bottom(&mut self) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
/// A range of all the lines that are visible given the renderer's size.
|
||||
pub fn visible_area<Id, R>(r: &R) -> Range<i32>
|
||||
where
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
let height: i32 = r.size().height.into();
|
||||
Range::new(0, height)
|
||||
}
|
||||
|
||||
/// The renderer's visible area, reduced by its scrolloff at the top and bottom.
|
||||
fn scroll_area<Id, R>(r: &R) -> Range<i32>
|
||||
where
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
let range = visible_area(r);
|
||||
let scrolloff = r.scrolloff();
|
||||
let top = range.top + scrolloff;
|
||||
let bottom = top.max(range.bottom - scrolloff);
|
||||
Range::new(top, bottom)
|
||||
}
|
||||
|
||||
/// Compute a delta that makes the object partially or fully overlap the area
|
||||
/// when added to the object. This delta should be as close to zero as possible.
|
||||
///
|
||||
/// If the object has a height of zero, it must be within the area or exactly on
|
||||
/// its border to be considered overlapping.
|
||||
///
|
||||
/// If the object has a nonzero height, at least one line of the object must be
|
||||
/// within the area for the object to be considered overlapping.
|
||||
fn overlap_delta(area: Range<i32>, object: Range<i32>) -> i32 {
|
||||
assert!(object.top <= object.bottom, "object range not well-formed");
|
||||
assert!(area.top <= area.bottom, "area range not well-formed");
|
||||
|
||||
if object.top == object.bottom || area.top == area.bottom {
|
||||
// Delta that moves the object.bottom to area.top. If this is positive,
|
||||
// we need to move the object because it is too high.
|
||||
let move_to_top = area.top - object.bottom;
|
||||
|
||||
// Delta that moves the object.top to area.bottom. If this is negative,
|
||||
// we need to move the object because it is too low.
|
||||
let move_to_bottom = area.bottom - object.top;
|
||||
|
||||
// move_to_top <= move_to_bottom because...
|
||||
//
|
||||
// Case 1: object.top == object.bottom
|
||||
// Premise follows from rom area.top <= area.bottom
|
||||
//
|
||||
// Case 2: area.top == area.bottom
|
||||
// Premise follows from object.top <= object.bottom
|
||||
0.clamp(move_to_top, move_to_bottom)
|
||||
} else {
|
||||
// Delta that moves object.bottom one line below area.top. If this is
|
||||
// positive, we need to move the object because it is too high.
|
||||
let move_to_top = (area.top + 1) - object.bottom;
|
||||
|
||||
// Delta that moves object.top one line above area.bottom. If this is
|
||||
// negative, we need to move the object because it is too low.
|
||||
let move_to_bottom = (area.bottom - 1) - object.top;
|
||||
|
||||
// move_to_top <= move_to_bottom because...
|
||||
//
|
||||
// We know that area.top < area.bottom and object.top < object.bottom,
|
||||
// otherwise we'd be in the previous `if` branch.
|
||||
//
|
||||
// We get the largest value for move_to_top if area.top is largest and
|
||||
// object.bottom is smallest. We get the smallest value for
|
||||
// move_to_bottom if area.bottom is smallest and object.top is largest.
|
||||
//
|
||||
// This means that the worst case scenario is when area.top and
|
||||
// area.bottom as well as object.top and object.bottom are closest
|
||||
// together. In other words:
|
||||
//
|
||||
// area.top + 1 == area.bottom
|
||||
// object.top + 1 == object.bottom
|
||||
//
|
||||
// Inserting that into our formulas for move_to_top and move_to_bottom,
|
||||
// we get:
|
||||
//
|
||||
// move_to_top = (area.top + 1) - (object.top + 1) = area.top + object.top
|
||||
// move_to_bottom = (area.top + 1 - 1) - object.top = area.top + object.top
|
||||
0.clamp(move_to_top, move_to_bottom)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overlaps(area: Range<i32>, object: Range<i32>) -> bool {
|
||||
overlap_delta(area, object) == 0
|
||||
}
|
||||
|
||||
/// Move the object such that it overlaps the area.
|
||||
fn overlap(area: Range<i32>, object: Range<i32>) -> Range<i32> {
|
||||
object.shifted(overlap_delta(area, object))
|
||||
}
|
||||
|
||||
/// Compute a delta that makes the object fully overlap the area when added to
|
||||
/// the object. This delta should be as close to zero as possible.
|
||||
///
|
||||
/// If the object is higher than the area, it should be moved such that
|
||||
/// object.top == area.top.
|
||||
fn full_overlap_delta(area: Range<i32>, object: Range<i32>) -> i32 {
|
||||
assert!(object.top <= object.bottom, "object range not well-formed");
|
||||
assert!(area.top <= area.bottom, "area range not well-formed");
|
||||
|
||||
// Delta that moves object.top to area.top. If this is positive, we need to
|
||||
// move the object because it is too high.
|
||||
let move_to_top = area.top - object.top;
|
||||
|
||||
// Delta that moves object.bottom to area.bottom. If this is negative, we
|
||||
// need to move the object because it is too low.
|
||||
let move_to_bottom = area.bottom - object.bottom;
|
||||
|
||||
// If the object is higher than the area, move_to_top becomes larger than
|
||||
// move_to_bottom. In that case, this function should return move_to_top.
|
||||
0.min(move_to_bottom).max(move_to_top)
|
||||
}
|
||||
|
||||
async fn expand_upwards_until<Id, R>(r: &mut R, top: i32) -> Result<(), R::Error>
|
||||
where
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
loop {
|
||||
let blocks = r.blocks();
|
||||
if blocks.end().top || blocks.range().top <= top {
|
||||
break;
|
||||
}
|
||||
|
||||
r.expand_top().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn expand_downwards_until<Id, R>(r: &mut R, bottom: i32) -> Result<(), R::Error>
|
||||
where
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
loop {
|
||||
let blocks = r.blocks();
|
||||
if blocks.end().bottom || blocks.range().bottom >= bottom {
|
||||
break;
|
||||
}
|
||||
|
||||
r.expand_bottom().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn expand_to_fill_visible_area<Id, R>(r: &mut R) -> Result<(), R::Error>
|
||||
where
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
let area = visible_area(r);
|
||||
expand_upwards_until(r, area.top).await?;
|
||||
expand_downwards_until(r, area.bottom).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Expand blocks such that the screen is full for any offset where the
|
||||
/// specified block is visible.
|
||||
pub async fn expand_to_fill_screen_around_block<Id, R>(r: &mut R, id: &Id) -> Result<(), R::Error>
|
||||
where
|
||||
Id: Eq,
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
let screen = visible_area(r);
|
||||
let (block, _) = r.blocks().find_block(id).expect("no block with that id");
|
||||
|
||||
let top = overlap(block, screen.with_bottom(block.top)).top;
|
||||
let bottom = overlap(block, screen.with_top(block.bottom)).bottom;
|
||||
|
||||
expand_upwards_until(r, top).await?;
|
||||
expand_downwards_until(r, bottom).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn scroll_to_set_block_top<Id, R>(r: &mut R, id: &Id, top: i32) -> bool
|
||||
where
|
||||
Id: Eq,
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
if let Some((range, _)) = r.blocks().find_block(id) {
|
||||
let delta = top - range.top;
|
||||
r.blocks_mut().shift(delta);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_so_block_is_centered<Id, R>(r: &mut R, id: &Id)
|
||||
where
|
||||
Id: Eq,
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
let area = visible_area(r);
|
||||
let (range, block) = r.blocks().find_block(id).expect("no block with that id");
|
||||
let focus = block.focus(range);
|
||||
let focus_height = focus.bottom - focus.top;
|
||||
let top = (area.top + area.bottom - focus_height) / 2;
|
||||
r.blocks_mut().shift(top - range.top);
|
||||
}
|
||||
|
||||
pub fn scroll_blocks_fully_above_screen<Id, R>(r: &mut R)
|
||||
where
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
let area = visible_area(r);
|
||||
let blocks = r.blocks_mut();
|
||||
let delta = area.top - blocks.range().bottom;
|
||||
blocks.shift(delta);
|
||||
}
|
||||
|
||||
pub fn scroll_blocks_fully_below_screen<Id, R>(r: &mut R)
|
||||
where
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
let area = visible_area(r);
|
||||
let blocks = r.blocks_mut();
|
||||
let delta = area.bottom - blocks.range().top;
|
||||
blocks.shift(delta);
|
||||
}
|
||||
|
||||
pub fn scroll_so_block_focus_overlaps_scroll_area<Id, R>(r: &mut R, id: &Id) -> bool
|
||||
where
|
||||
Id: Eq,
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
if let Some((range, block)) = r.blocks().find_block(id) {
|
||||
let area = scroll_area(r);
|
||||
let delta = overlap_delta(area, block.focus(range));
|
||||
r.blocks_mut().shift(delta);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_so_block_focus_fully_overlaps_scroll_area<Id, R>(r: &mut R, id: &Id) -> bool
|
||||
where
|
||||
Id: Eq,
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
if let Some((range, block)) = r.blocks().find_block(id) {
|
||||
let area = scroll_area(r);
|
||||
let delta = full_overlap_delta(area, block.focus(range));
|
||||
r.blocks_mut().shift(delta);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clamp_scroll_biased_upwards<Id, R>(r: &mut R)
|
||||
where
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
let area = visible_area(r);
|
||||
let blocks = r.blocks().range();
|
||||
|
||||
// Delta that moves blocks.top to the top of the screen. If this is
|
||||
// negative, we need to move the blocks because they're too low.
|
||||
let move_to_top = blocks.top - area.top;
|
||||
|
||||
// Delta that moves blocks.bottom to the bottom of the screen. If this is
|
||||
// positive, we need to move the blocks because they're too high.
|
||||
let move_to_bottom = blocks.bottom - area.bottom;
|
||||
|
||||
// If the screen is higher, the blocks should rather be moved to the top
|
||||
// than the bottom because of the upwards bias.
|
||||
let delta = 0.max(move_to_bottom).min(move_to_top);
|
||||
r.blocks_mut().shift(delta);
|
||||
}
|
||||
|
||||
pub fn clamp_scroll_biased_downwards<Id, R>(r: &mut R)
|
||||
where
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
let area = visible_area(r);
|
||||
let blocks = r.blocks().range();
|
||||
|
||||
// Delta that moves blocks.top to the top of the screen. If this is
|
||||
// negative, we need to move the blocks because they're too low.
|
||||
let move_to_top = area.top - blocks.top;
|
||||
|
||||
// Delta that moves blocks.bottom to the bottom of the screen. If this is
|
||||
// positive, we need to move the blocks because they're too high.
|
||||
let move_to_bottom = area.bottom - blocks.bottom;
|
||||
|
||||
// If the screen is higher, the blocks should rather be moved to the bottom
|
||||
// than the top because of the downwards bias.
|
||||
let delta = 0.min(move_to_top).max(move_to_bottom);
|
||||
r.blocks_mut().shift(delta);
|
||||
}
|
||||
|
||||
pub fn find_cursor_starting_at<'a, Id, R>(r: &'a R, id: &Id) -> Option<&'a Id>
|
||||
where
|
||||
Id: Eq,
|
||||
R: Renderer<Id>,
|
||||
{
|
||||
let area = scroll_area(r);
|
||||
let (range, block) = r.blocks().find_block(id)?;
|
||||
let delta = overlap_delta(area, block.focus(range));
|
||||
match delta.cmp(&0) {
|
||||
Ordering::Equal => Some(block.id()),
|
||||
|
||||
// Blocks must be scrolled downwards to become visible, meaning the
|
||||
// cursor must be above the visible area.
|
||||
Ordering::Greater => r
|
||||
.blocks()
|
||||
.iter()
|
||||
.filter(|(_, block)| block.can_be_cursor())
|
||||
.find(|(range, block)| overlaps(area, block.focus(*range)))
|
||||
.map(|(_, block)| block.id()),
|
||||
|
||||
// Blocks must be scrolled upwards to become visible, meaning the cursor
|
||||
// must be below the visible area.
|
||||
Ordering::Less => r
|
||||
.blocks()
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|(_, block)| block.can_be_cursor())
|
||||
.find(|(range, block)| overlaps(area, block.focus(*range)))
|
||||
.map(|(_, block)| block.id()),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue