Can't auto-scroll text view in gtk4-rs

I’m creating an app using relm4 and gtk-rs crates in Rust. I creates a textview so I can show app’s logs and info in it, I want it to auto scroll to the end when new lines are added. Here’s my update() function:

    fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
        match message {
            SimpleTextViewInput::Append(text) => {
                               
                let mut end_iter = self.buffer.end_iter();
                let log_level = ["ERROR", "WARN", "INFO", "DEBUG", "TRACE"]
                    .iter()
                    .find(|&&level| text.contains(&format!("[{}]", level)))
                    .unwrap_or(&"INFO");

                self.buffer.insert_with_tags_by_name(
                    &mut end_iter,
                    &text.to_string(),
                    &[log_level],
                );

                // Log rotation
                let line_count = self.buffer.line_count();
                if line_count > MAX_LINES {
                    let lines_to_remove = line_count - MAX_LINES;
                    let mut start = self.buffer.start_iter();
                    let mut end = self.buffer.iter_at_line(lines_to_remove).unwrap();
                    self.buffer.delete(&mut start, &mut end);
                }

                // Create a mark at the end of the buffer
                let end_iter = self.buffer.end_iter();
                let end_mark = self.buffer.create_mark(Some("end"), &end_iter, false);

                // Scroll to make the end mark visible
                self.view.scroll_mark_onscreen(&end_mark);

                // Delete the mark after scrolling
                self.buffer.delete_mark(&end_mark);
            }
        }
    }

Auto-scrolling doesn’t work and this line self.view.scroll_mark_onscreen(&end_mark); causes errors:

(mmw_visualizer:186500): Gtk-CRITICAL **: 12:54:27.173: gtk_text_view_scroll_mark_onscreen: assertion 'get_buffer (text_view) == gtk_text_mark_get_buffer (mark)' failed

(mmw_visualizer:186500): Gtk-CRITICAL **: 12:54:27.173: gtk_text_view_scroll_mark_onscreen: assertion 'get_buffer (text_view) == gtk_text_mark_get_buffer (mark)' failed

(mmw_visualizer:186500): Gtk-CRITICAL **: 12:54:27.173: gtk_text_view_scroll_mark_onscreen: assertion 'get_buffer (text_view) == gtk_text_mark_get_buffer (mark)' failed

I’m not sure what to do with it, any ideas what could go wrong?

Hi,

When inserting text in a textview, there are some validation done in background, asynchronously. Only after the validation is done, the scrolling is processed.

Creating and deleting marks using the same name seems to confuse the textview… (I suppose a reference is kept for the async processing, but the succession of create/delete leads to some kind of race condition).
Usually, we create marks only once, no need to recreate them every time.

There is a good example in the gtk demos: demos/gtk-demo/textscroll.c · main · GNOME / gtk · GitLab

1 Like

Thanks! I managed to get it work.
Here’s Rust code, that uses Relm4 + gtk-rs + async:

use gtk4::prelude::*;
use relm4::prelude::*;
use gtk4::glib;
use std::time::Duration;
use chrono::Local;
use tokio::sync::mpsc::{self, Sender};

#[derive(Debug)]
struct AppModel {
    tx: Sender<String>,
}

#[derive(Debug)]
enum AppMsg {
    Append(String),
}

#[relm4::component(async)]
impl SimpleAsyncComponent for AppModel {
    type Init = ();
    type Input = AppMsg;
    type Output = ();

    view! {
        gtk4::ApplicationWindow {
            set_title: Some("Automatic Scrolling"),
            set_default_width: 400,
            set_default_height: 300,

            gtk4::ScrolledWindow {
                set_hscrollbar_policy: gtk4::PolicyType::Never,
                #[name = "text_view"]
                gtk4::TextView {
                    set_editable: false,
                    set_cursor_visible: false,
                    set_wrap_mode: gtk4::WrapMode::Word,
                }
            }
        }
    }

    async fn init(
        _init: Self::Init,
        root: Self::Root,
        sender: AsyncComponentSender<Self>,
    ) -> AsyncComponentParts<Self> {
        let widgets = view_output!();
        let text_view = &widgets.text_view;
        let (tx, rx) = mpsc::channel(100);
        let model = AppModel { tx };
        setup_scroll(text_view, rx);
        start_background_task(sender.clone());

        AsyncComponentParts { model, widgets }
    }

    async fn update(&mut self, msg: Self::Input, _sender: AsyncComponentSender<Self>) {
        match msg {
            AppMsg::Append(text) => {
                self.tx.send(text).await.unwrap();
            }
        }
    }
}

fn setup_scroll(text_view: &gtk4::TextView, mut receiver: mpsc::Receiver<String>) {
    let buffer = text_view.buffer();
    let end_mark = buffer.create_mark(Some("end"), &buffer.end_iter(), false);
    let text_view = text_view.clone();

    glib::spawn_future_local(async move {
        while let Some(text) = receiver.recv().await {
            let buffer = text_view.buffer();
            let mut iter = buffer.end_iter();
            buffer.insert(&mut iter, &format!("{}\n", text));
            text_view.scroll_mark_onscreen(&end_mark);
        }
    });
}

fn start_background_task(sender: AsyncComponentSender<AppModel>) {
    glib::spawn_future_local(async move {
        loop {
            let current_time = Local::now().format("%Y-%m-%d %H:%M:%S%.3f").to_string();
            sender.input(AppMsg::Append(current_time));
            glib::timeout_future(Duration::from_millis(100)).await;
        }
    });
}


fn main() {
    let app = RelmApp::new("com.example.TextScroll");
    app.run_async::<AppModel>(());
}

1 Like

This topic was automatically closed 45 days after the last reply. New replies are no longer allowed.