Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overflow scroll support for fullscreen? #68

Closed
AriaFallah opened this issue Mar 15, 2025 · 4 comments
Closed

Overflow scroll support for fullscreen? #68

AriaFallah opened this issue Mar 15, 2025 · 4 comments
Labels
enhancement New feature or request

Comments

@AriaFallah
Copy link

AriaFallah commented Mar 15, 2025

Hi! Thanks for making this library. It's very cool and exactly what I'd want.

I assume this isn't so trivial, but I was wondering if there's a way to handle text that overflows? The context is that I want to build a "hello world" sort of fullscreen chat application, and I'm not sure how to handle when there's more message than the size of a view can support. I saw there's https://docs.rs/iocraft/0.6.4/iocraft/enum.Overflow.html, but I think that's just inherited from taffy.

Do you have any examples of how to do scrolling?

@ccbrown
Copy link
Owner

ccbrown commented Mar 15, 2025

I think I re-exported Overflow at some point but forgot to finish implementing it fully.

I went ahead and took care of that in #70 (now released in version 0.7.0) and added a basic scrolling example to the examples directory:

Image

That should get you on the right track. The same sort of idea should work in full screen and with scroll wheel input.

Let me know if you have any issues or if that doesn't meet your need!

@ccbrown ccbrown closed this as completed Mar 15, 2025
@AriaFallah
Copy link
Author

That was insanely fast wow. Thanks so much! Will try it out soon :D

@AriaFallah
Copy link
Author

Ok got a chance to play with it. So the way you're doing it is like this? The overflow hidden is like a window, and you basically just slide the inner div up and down through the window using top?

Image

Is there a way to stay scrolled to the bottom if the text is dynamically changing? I tried something like top: Inset::Percent(100f32), but that didn't work. Like if we change the scroll example like this?

diff --git a/examples/scrolling.rs b/examples/scrolling.rs
index 3cb8e4d..e887b64 100644
--- a/examples/scrolling.rs
+++ b/examples/scrolling.rs
@@ -1,3 +1,5 @@
+use std::time::Duration;
+
 use iocraft::prelude::*;
 
 #[derive(Default, Props)]
@@ -10,6 +12,7 @@ fn Example<'a>(props: &Props<'a>, mut hooks: Hooks) -> impl Into<AnyElement<'sta
     let mut system = hooks.use_context_mut::<SystemContext>();
     let mut scroll_offset = hooks.use_state(|| 0i32);
     let mut should_exit = hooks.use_state(|| false);
+    let mut text = hooks.use_state(|| props.text.to_owned());
 
     hooks.use_terminal_events({
         move |event| match event {
@@ -25,6 +28,14 @@ fn Example<'a>(props: &Props<'a>, mut hooks: Hooks) -> impl Into<AnyElement<'sta
         }
     });
 
+    hooks.use_future(async move {
+        loop {
+            smol::Timer::after(Duration::from_secs(1)).await;
+            let mut text = text.write();
+            *text = text.to_owned() + "Testing!\n";
+        }
+    });
+
     if should_exit.get() {
         system.exit();
     }
@@ -45,10 +56,9 @@ fn Example<'a>(props: &Props<'a>, mut hooks: Hooks) -> impl Into<AnyElement<'sta
                 overflow: Overflow::Hidden,
             ) {
                 View(
-                    position: Position::Absolute,
                     top: -scroll_offset.get(),
                 ) {
-                    Text(content: props.text)
+                    Text(content: text.read().to_owned())
                 }
             }
         }

@ccbrown
Copy link
Owner

ccbrown commented Mar 16, 2025

Yeah, if you want the view to stay anchored at the bottom, just use bottom instead of top, like so:

diff --git a/examples/scrolling.rs b/examples/scrolling.rs
index 35adb68..9522a82 100644
--- a/examples/scrolling.rs
+++ b/examples/scrolling.rs
@@ -18,8 +18,8 @@ fn Example<'a>(props: &Props<'a>, mut hooks: Hooks) -> impl Into<AnyElement<'sta
             TerminalEvent::Key(KeyEvent { code, kind, .. }) if kind != KeyEventKind::Release => {
                 match code {
                     KeyCode::Char('q') => should_exit.set(true),
-                    KeyCode::Up => scroll_offset.set((scroll_offset.get() - 1).max(0)),
-                    KeyCode::Down => scroll_offset.set(scroll_offset.get() + 1),
+                    KeyCode::Down => scroll_offset.set((scroll_offset.get() - 1).max(0)),
+                    KeyCode::Up => scroll_offset.set(scroll_offset.get() + 1),
                     _ => {}
                 }
             }
@@ -56,7 +56,7 @@ fn Example<'a>(props: &Props<'a>, mut hooks: Hooks) -> impl Into<AnyElement<'sta
             ) {
                 View(
                     position: Position::Absolute,
-                    top: -scroll_offset.get(),
+                    bottom: -scroll_offset.get(),
                 ) {
                     Text(content: text.read().to_owned())
                 }

I imagine next you'll want to make sure that if the view isn't scrolled all the way to the bottom, it stays put so the visible text doesn't shift on the user. This isn't nearly as easy as I'd like it to be, but it is doable:

The general approach would be this: If the user is scrolling, use top to anchor the content at the top. Otherwise, if the user is scrolled all the way down, use bottom to automatically scroll when new content is added.

To do this, you'll need to do some math based on the height of the content.

We can make a custom hook to get a view's current height:

pub trait UseComponentHeight {
    fn use_component_height(&mut self) -> u16;
}

impl<'a> UseComponentHeight for Hooks<'a, '_> {
    fn use_component_height(&mut self) -> u16 {
        self.use_hook(move || UseComponentHeightImpl(0)).0
    }
}

struct UseComponentHeightImpl(u16);

impl Hook for UseComponentHeightImpl {
    fn pre_component_draw(&mut self, drawer: &mut ComponentDrawer) {
        self.0 = drawer.size().height;
    }
}

Then, we can make a ContentView component, which will do two things:

  1. Expose its current height to its parent.
  2. Switch between two scrolling modes: Auto and Top(offset).
#[derive(Clone, Copy, Default, PartialEq)]
enum Scrolling {
    #[default]
    Auto,
    Top(i32),
}

#[derive(Default, Props)]
struct ContentViewProps<'a> {
    children: Vec<AnyElement<'a>>,
    scrolling: Scrolling,
    content_height_out: Option<State<u16>>,
}

#[component]
fn ContentView<'a>(
    props: &mut ContentViewProps<'a>,
    mut hooks: Hooks,
) -> impl Into<AnyElement<'a>> {
    let height = hooks.use_component_height();
    if let Some(out) = props.content_height_out.as_mut() {
        if height != out.get() {
            out.set(height);
        }
    }

    element! {
        View(
            position: Position::Absolute,
            bottom: if props.scrolling == Scrolling::Auto {
                Inset::Length(0)
            } else {
                Inset::Auto
            },
            top: if let Scrolling::Top(offset) = props.scrolling {
                Inset::Length(-(offset as i32))
            } else {
                Inset::Auto
            },
        ) {
            #(props.children.iter_mut())
        }
    }
}

Then its parent can implement the math to switch between modes gracefully based on whether the user is scrolling:

#[derive(Default, Props)]
struct Props<'a> {
    text: &'a str,
}

#[component]
fn Example<'a>(props: &Props<'a>, mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
    let mut system = hooks.use_context_mut::<SystemContext>();
    let mut scrolling = hooks.use_state(|| Scrolling::Auto);
    let mut should_exit = hooks.use_state(|| false);
    let mut text = hooks.use_state(|| props.text.to_owned());
    let content_height = hooks.use_state(|| 0);

    let scroll_area_height = 8;

    hooks.use_terminal_events({
        move |event| match event {
            TerminalEvent::Key(KeyEvent { code, kind, .. }) if kind != KeyEventKind::Release => {
                match code {
                    KeyCode::Char('q') => should_exit.set(true),
                    KeyCode::Down => {
                        if let Scrolling::Top(offset) = scrolling.get() {
                            if offset >= content_height.get() as i32 - scroll_area_height as i32 {
                                scrolling.set(Scrolling::Auto);
                            } else {
                                scrolling.set(Scrolling::Top(offset + 1));
                            }
                        }
                    }
                    KeyCode::Up => match scrolling.get() {
                        Scrolling::Auto => {
                            scrolling.set(Scrolling::Top(
                                (content_height.get() as i32 - scroll_area_height as i32 - 1)
                                    .max(0),
                            ));
                        }
                        Scrolling::Top(offset) => scrolling.set(Scrolling::Top(offset.max(1) - 1)),
                    },
                    _ => {}
                }
            }
            _ => {}
        }
    });

    hooks.use_future(async move {
        loop {
            smol::Timer::after(Duration::from_secs(1)).await;
            let mut text = text.write();
            *text = text.to_owned() + "Testing!\n";
        }
    });

    if should_exit.get() {
        system.exit();
    }

    element! {
        View(
            flex_direction: FlexDirection::Column,
            padding: 2,
            align_items: AlignItems::Center
        ) {
            Text(content: "Use arrow keys to scroll. Press \"q\" to exit.")
            View(
                border_style: BorderStyle::DoubleLeftRight,
                border_color: Color::Green,
                margin: 1,
                width: 78,
                height: scroll_area_height + 2,
                overflow: Overflow::Hidden,
            ) {
                ContentView(
                    scrolling: scrolling.get(),
                    content_height_out: content_height,
                ) {
                    Text(content: text.read().to_owned())
                }
            }
        }
    }
}

scrolling.rs.zip

A built-in component for easy scroll views would definitely be a welcome addition, and I'll likely add them it some point. Creating a new issue here: #71

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants