Skip to content

Modifier and Blink Support#172

Merged
orhun merged 54 commits intoratatui:mainfrom
gold-silver-copper:main
Mar 19, 2026
Merged

Modifier and Blink Support#172
orhun merged 54 commits intoratatui:mainfrom
gold-silver-copper:main

Conversation

@gold-silver-copper
Copy link
Copy Markdown
Contributor

@gold-silver-copper gold-silver-copper commented Feb 22, 2026

This PR implements all modifier types, adds cursor and blinking support, along with configuration structs for blinking and cursor.

I split the current draw function into draw and draw_cell, to be able to reuse the draw_cell logic for blinking. Blinking requires keeping track of cells which must be redrawn, as far as I am aware this is what terminal emulators do on their side. It is necessary because the draw function only draws changed cells, blinking cells arent fundamentally changed per blink, so they dont get caught by the draw function. The cursor drawing is done inside flush() due to the way ratatui handles drawing, adding the cursor to the draw() function did not work. I am using a BTreeMap cause normal Hashmap not available in alloc.

Something to discuss about Blink implementations: we need to support some sort of timing. There are crates such as embedded_time which looks pretty good but hasn't been updated in almost 5 years and would be an extra dependency. And the user/us would have to provide an impl for every supported device.

An alternative to using proper timing, we can add a frame counter field to the backend struct. This is what this PR uses.

PS: I stole some of the cursor code from jagoda's cursor PR, im not very good at git stuff so i didnt know how to merge it into my branch oops

This PR closes:
#58
#10
#35

Copy link
Copy Markdown
Member

@orhun orhun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool!

@gold-silver-copper
Copy link
Copy Markdown
Contributor Author

gold-silver-copper commented Feb 23, 2026

Current summary of my thoughts and notes:
Updated gif and sample code to show cursor moving around.
The MCUs I had SUCK and couldnt run mousefood so I ordered some new ones for testing, should have them by end of the week.

Adding frame_count field to the backend is neccesary for blinking. ratatui::Frame.count() exists but we cannot use it from the backend. Perhaps it would be nice in the future for ratatui to support exposing a general frame count on the backend, but thats a change to core beyond this PR.

I added Cursor style and configuration structs, as well as Blink timing and config structs. Should make code cleaner and allow future customization options. Four cursor types are currently provided, inverse cursor fallbacks to underline when framebuffer unavailable. The blink configs hold some state.

Should probably add tests, tests are always nice. Performance should also be checked to avoid regression. Feature gating blinking is an option if it makes perf really bad.

Copy link
Copy Markdown
Member

@j-g00da j-g00da left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few thoughts:

I would lock blinking behind a feature flag anyway, it's additional computation and memory usage that in most cases won't be used.

I would also try to avoid using floats if not necessary, maybe rounding it to full ints would be good enough?

frame_count probably doesn't have to be usize, u8 may be enough.

EmbeddedBackendConfig is exhaustive, so this should be marked as a breaking change (and described in BREAKING_CHANGES.md)

@gold-silver-copper
Copy link
Copy Markdown
Contributor Author

gold-silver-copper commented Feb 25, 2026

Added a blink feature flag, put all blink related code behind it. Disabled by default.

Removed use of floats

frame_count changed to u16, the blink algorithm requires multiplication that would overflow on u8, I think storing as u16 is better than casting to u16 every update, imo. frame_count is also feature flagged

Entry added to breaking changes md, with migration guide.

gold-silver-copper and others added 4 commits February 25, 2026 21:02
Co-authored-by: Orhun Parmaksız <[email protected]>
Co-authored-by: Orhun Parmaksız <[email protected]>
Co-authored-by: Orhun Parmaksız <[email protected]>
@gold-silver-copper
Copy link
Copy Markdown
Contributor Author

Created cursor struct, cursor.rs module, moved Result type to error.rs for shared usage. I will work on condensing the demo I have and creating a nice gif for it.

Currently the simulator screenshot inside the README is very similar to the screenshot right above it of the italics font. I am thinking of replacing the README simulator screenshot with a small gif showcasing blinking and cursor moving around, inside the simulator.

But I am not sure if we should modify the code within the simulator demo, since having a bare minimum example is kind of nice, but idk, thoughts?

@gold-silver-copper
Copy link
Copy Markdown
Contributor Author

gold-silver-copper commented Feb 26, 2026

gif and some sample code

output

//! # Simulator
//!
//! A window will open with the simulator running.
//! Use arrow keys or WASD to move the cursor.

use std::cell::RefCell;
use std::rc::Rc;

use embedded_graphics_simulator::{
    OutputSettings, SimulatorDisplay, SimulatorEvent, Window, sdl2::Keycode,
};
use mousefood::embedded_graphics::geometry;
use mousefood::embedded_graphics::prelude::RgbColor;
use mousefood::error::Error;
use mousefood::{CursorConfig, CursorStyle, prelude::*};
use ratatui::backend::Backend;
use ratatui::widgets::{Block, Paragraph, Wrap};
use ratatui::{Frame, Terminal, style::*};

fn main() -> Result<(), Error> {
    let mut simulator_window = Window::new(
        "mousefood simulator",
        &OutputSettings {
            scale: 4,
            ..Default::default()
        },
    );
    simulator_window.set_max_fps(30);

    let mut display = SimulatorDisplay::<Bgr565>::new(geometry::Size::new(128, 64));

    let events: Rc<RefCell<Vec<SimulatorEvent>>> = Rc::new(RefCell::new(Vec::new()));
    let events_cb = events.clone();

    let backend_config = EmbeddedBackendConfig {
        flush_callback: Box::new(move |display| {
            simulator_window.update(display);
            let mut ev = events_cb.borrow_mut();
            ev.clear();
            ev.extend(simulator_window.events());
        }),
        color_theme: ColorTheme::tokyo_night(),
        cursor: CursorConfig {
            style: CursorStyle::Inverse,
            blink: true,
            color: Rgb888::WHITE,
        },
        #[cfg(feature = "blink")]
        blink: BlinkConfig {
            fps: 30,
            slow: BlinkTiming {
                blinks_per_sec: 1,
                duty_percent: 15,
            },
            fast: BlinkTiming {
                blinks_per_sec: 3,
                duty_percent: 50,
            },
        },
        ..Default::default()
    };
    let backend: EmbeddedBackend<SimulatorDisplay<_>, _> =
        EmbeddedBackend::new(&mut display, backend_config);

    let mut terminal = Terminal::new(backend)?;

    let mut frame_count: usize = 0;
    let mut cursor_x: u16 = 1;
    let mut cursor_y: u16 = 1;

    loop {
        let count = frame_count;
        let cx = cursor_x;
        let cy = cursor_y;
        terminal.draw(|frame| draw2(frame, count, cx, cy))?;
        frame_count = frame_count.wrapping_add(1);

        for event in events.borrow().iter() {
            match event {
                SimulatorEvent::KeyDown { keycode, .. } => match *keycode {
                    Keycode::Up | Keycode::W => {
                        cursor_y = cursor_y.saturating_sub(1);
                    }
                    Keycode::Down | Keycode::S => {
                        cursor_y = cursor_y.saturating_add(1);
                    }
                    Keycode::Left | Keycode::A => {
                        cursor_x = cursor_x.saturating_sub(1);
                    }
                    Keycode::Right | Keycode::D => {
                        cursor_x = cursor_x.saturating_add(1);
                    }
                    _ => {}
                },
                SimulatorEvent::Quit => return Ok(()),
                _ => {}
            }
        }

        if let Ok(size) = terminal.backend_mut().size() {
            cursor_x = cursor_x.min(size.width.saturating_sub(1));
            cursor_y = cursor_y.min(size.height.saturating_sub(1));
        }
    }
}

fn draw2(frame: &mut Frame, count: usize, cursor_x: u16, cursor_y: u16) {
    use ratatui::style::Modifier;
    use ratatui::text::{Line, Span};

    let line = Line::from(vec![
        Span::styled(format!("F:{count} "), Style::new().yellow()),
        Span::styled("RED ", Style::new().fg(Color::Red)),
        Span::styled(
            "DIM ",
            Style::new().fg(Color::Red).add_modifier(Modifier::DIM),
        ),
        Span::styled("UNDR ", Style::new().add_modifier(Modifier::UNDERLINED)),
        Span::styled("SLOW ", Style::new().add_modifier(Modifier::SLOW_BLINK)),
        Span::styled("FAST ", Style::new().add_modifier(Modifier::RAPID_BLINK)),
        Span::styled("REV ", Style::new().add_modifier(Modifier::REVERSED)),
        Span::styled("HIDE ", Style::new().add_modifier(Modifier::HIDDEN)),
        Span::styled("XOUT ", Style::new().add_modifier(Modifier::CROSSED_OUT)),
        Span::styled(
            "D+U ",
            Style::new().add_modifier(Modifier::DIM | Modifier::UNDERLINED),
        ),
        // combos
        Span::styled(
            "GHOST ",
            Style::new()
                .fg(Color::DarkGray)
                .add_modifier(Modifier::DIM | Modifier::ITALIC),
        ),
        Span::styled(
            "ALARM ",
            Style::new()
                .fg(Color::Red)
                .add_modifier(Modifier::RAPID_BLINK | Modifier::REVERSED),
        ),
        Span::styled(
            "DEAD ",
            Style::new()
                .fg(Color::Gray)
                .add_modifier(Modifier::CROSSED_OUT | Modifier::DIM),
        ),
        Span::styled(
            "SHOUT ",
            Style::new()
                .fg(Color::Yellow)
                .add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
        ),
        Span::styled(
            "HAUNT ",
            Style::new()
                .fg(Color::Magenta)
                .add_modifier(Modifier::SLOW_BLINK | Modifier::DIM),
        ),
        Span::styled(
            "CRIT",
            Style::new()
                .fg(Color::White)
                .bg(Color::Red)
                .add_modifier(Modifier::BOLD | Modifier::RAPID_BLINK),
        ),
    ]);

    let paragraph = Paragraph::new(vec![line]).wrap(Wrap { trim: true });
    let bordered_block = Block::bordered()
        .border_style(Style::new().yellow())
        .title("Mods");
    frame.render_widget(paragraph.block(bordered_block), frame.area());
    frame.set_cursor_position((cursor_x, cursor_y));
}
[package]
name = "simulator"
edition.workspace = true
license.workspace = true
publish = false

[dependencies]
mousefood = { path = "../../mousefood/" , features = ["blink"]}
embedded-graphics-simulator.workspace = true
ratatui.workspace = true

@orhun
Copy link
Copy Markdown
Member

orhun commented Feb 26, 2026

I like how the demo you posted looks like!

But I am not sure if we should modify the code within the simulator demo, since having a bare minimum example is kind of nice, but idk, thoughts?

Agreed. One option is extracting all the demo UI elements into a new module and simply calling a function from the main file. Or we could have multiple binaries in the simulator app, so that the user can run --bin cursor, --bin minimal etc. Or maybe even examples? (--example?)

@gold-silver-copper
Copy link
Copy Markdown
Contributor Author

I replaced the simulator screenshot in the readme with a gif.
I added the modifiers demo as an extra bin to the simulator folder.

Got it running on hardware:
hi

Copy link
Copy Markdown
Member

@orhun orhun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

awesome work

@gold-silver-copper
Copy link
Copy Markdown
Contributor Author

Applied suggested changes by orhun

Copy link
Copy Markdown
Member

@j-g00da j-g00da left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good, thanks! :)

@j-g00da j-g00da mentioned this pull request Mar 11, 2026
9 tasks
@orhun orhun merged commit 3a3904b into ratatui:main Mar 19, 2026
27 checks passed
@github-actions github-actions bot mentioned this pull request Mar 19, 2026
This was referenced Mar 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants