feat!: add get/set_cursor_position() methods to Terminal and Backend (#1284)

The new methods return/accept `Into<Position>` which can be either a Position or a (u16, u16) tuple.

```rust
backend.set_cursor_position(Position { x: 0, y: 20 })?;
let position = backend.get_cursor_position()?;
terminal.set_cursor_position((0, 20))?;
let position = terminal.set_cursor_position()?;
```
This commit is contained in:
EdJoPaTo 2024-08-06 13:10:28 +02:00 committed by GitHub
parent afe15349c8
commit c68ee6c64a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 190 additions and 90 deletions

View file

@ -11,10 +11,12 @@ GitHub with a [breaking change] label.
This is a quick summary of the sections below:
- [v0.28.0](#v0280) (unreleased)
`Backend::size` returns `Size` instead of `Rect`
- `Backend` trait migrates to `get/set_cursor_position`
- Ratatui now requires Crossterm 0.28.0
- `Axis::labels` now accepts `IntoIterator<Into<Line>>`
- `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize`
- `ratatui::terminal` module is now private
- `Axis::labels` now accepts `IntoIterator<Into<Line>>`
- `ToText` no longer has a lifetime
- [v0.27.0](#v0270)
- List no clamps the selected index to list
@ -71,6 +73,17 @@ This is a quick summary of the sections below:
The `Backend::size` method returns a `Size` instead of a `Rect`.
There is no need for the position here as it was always 0,0.
### `Backend` trait migrates to `get/set_cursor_position` ([#1284])
[#1284]: https://github.com/ratatui-org/ratatui/pull/1284
If you just use the types implementing the `Backend` trait, you will see deprecation hints but
nothing is a breaking change for you.
If you implement the Backend trait yourself, you need to update the implementation and add the
`get/set_cursor_position` method. You can remove the `get/set_cursor` methods as they are deprecated
and a default implementation for them exists.
### Ratatui now requires Crossterm 0.28.0 ([#1278])
[#1278]: https://github.com/ratatui-org/ratatui/pull/1278

View file

@ -36,7 +36,7 @@ use ratatui::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Constraint, Layout},
layout::{Constraint, Layout, Position},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span, Text},
widgets::{Block, List, ListItem, Paragraph},
@ -245,22 +245,19 @@ fn ui(f: &mut Frame, app: &App) {
.block(Block::bordered().title("Input"));
f.render_widget(input, input_area);
match app.input_mode {
InputMode::Normal =>
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
{}
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
InputMode::Normal => {}
InputMode::Editing => {
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
// rendering
#[allow(clippy::cast_possible_truncation)]
f.set_cursor(
// Draw the cursor at the current position in the input field.
// This position is can be controlled via the left and right arrow key
input_area.x + app.character_index as u16 + 1,
// Move one line down, from the border to the input line
input_area.y + 1,
);
}
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
// rendering
#[allow(clippy::cast_possible_truncation)]
InputMode::Editing => f.set_cursor_position(Position::new(
// Draw the cursor at the current position in the input field.
// This position is can be controlled via the left and right arrow key
input_area.x + app.character_index as u16 + 1,
// Move one line down, from the border to the input line
input_area.y + 1,
)),
}
let messages: Vec<ListItem> = app

View file

@ -104,7 +104,10 @@ use std::io;
use strum::{Display, EnumString};
use crate::{buffer::Cell, layout::Size};
use crate::{
buffer::Cell,
layout::{Position, Size},
};
#[cfg(feature = "termion")]
mod termion;
@ -192,25 +195,25 @@ pub trait Backend {
/// # std::io::Result::Ok(())
/// ```
///
/// [`show_cursor`]: Backend::show_cursor
/// [`show_cursor`]: Self::show_cursor
fn hide_cursor(&mut self) -> io::Result<()>;
/// Show the cursor on the terminal screen.
///
/// See [`hide_cursor`] for an example.
///
/// [`hide_cursor`]: Backend::hide_cursor
/// [`hide_cursor`]: Self::hide_cursor
fn show_cursor(&mut self) -> io::Result<()>;
/// Get the current cursor position on the terminal screen.
///
/// The returned tuple contains the x and y coordinates of the cursor. The origin
/// (0, 0) is at the top left corner of the screen.
/// The returned tuple contains the x and y coordinates of the cursor.
/// The origin (0, 0) is at the top left corner of the screen.
///
/// See [`set_cursor`] for an example.
/// See [`set_cursor_position`] for an example.
///
/// [`set_cursor`]: Backend::set_cursor
fn get_cursor(&mut self) -> io::Result<(u16, u16)>;
/// [`set_cursor_position`]: Self::set_cursor_position
fn get_cursor_position(&mut self) -> io::Result<Position>;
/// Set the cursor position on the terminal screen to the given x and y coordinates.
///
@ -220,14 +223,31 @@ pub trait Backend {
///
/// ```rust
/// # use ratatui::backend::{Backend, TestBackend};
/// # use ratatui::layout::Position;
/// # let mut backend = TestBackend::new(80, 25);
/// backend.set_cursor(10, 20)?;
/// assert_eq!(backend.get_cursor()?, (10, 20));
/// backend.set_cursor_position(Position { x: 10, y: 20 })?;
/// assert_eq!(backend.get_cursor_position()?, Position { x: 10, y: 20 });
/// # std::io::Result::Ok(())
/// ```
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()>;
/// Get the current cursor position on the terminal screen.
///
/// [`get_cursor`]: Backend::get_cursor
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()>;
/// The returned tuple contains the x and y coordinates of the cursor. The origin
/// (0, 0) is at the top left corner of the screen.
#[deprecated = "the method get_cursor_position indicates more clearly what about the cursor to get"]
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
let Position { x, y } = self.get_cursor_position()?;
Ok((x, y))
}
/// Set the cursor position on the terminal screen to the given x and y coordinates.
///
/// The origin (0, 0) is at the top left corner of the screen.
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.set_cursor_position(Position { x, y })
}
/// Clears the whole terminal screen
///
@ -261,7 +281,7 @@ pub trait Backend {
/// This method will return an error if the terminal screen could not be cleared. It will also
/// return an error if the `clear_type` is not supported by the backend.
///
/// [`clear`]: Backend::clear
/// [`clear`]: Self::clear
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
match clear_type {
ClearType::All => self.clear(),

View file

@ -212,12 +212,14 @@ where
execute!(self.writer, Show)
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
fn get_cursor_position(&mut self) -> io::Result<Position> {
crossterm::cursor::position()
.map(|(x, y)| Position { x, y })
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
let Position { x, y } = position.into();
execute!(self.writer, MoveTo(x, y))
}

View file

@ -157,11 +157,13 @@ where
self.writer.flush()
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
termion::cursor::DetectCursorPos::cursor_pos(&mut self.writer).map(|(x, y)| (x - 1, y - 1))
fn get_cursor_position(&mut self) -> io::Result<Position> {
termion::cursor::DetectCursorPos::cursor_pos(&mut self.writer)
.map(|(x, y)| Position { x: x - 1, y: y - 1 })
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
let Position { x, y } = position.into();
write!(self.writer, "{}", termion::cursor::Goto(x + 1, y + 1))?;
self.writer.flush()
}

View file

@ -191,12 +191,16 @@ impl Backend for TermwizBackend {
Ok(())
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
fn get_cursor_position(&mut self) -> io::Result<crate::layout::Position> {
let (x, y) = self.buffered_terminal.cursor_position();
Ok((x as u16, y as u16))
Ok((x as u16, y as u16).into())
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
fn set_cursor_position<P: Into<crate::layout::Position>>(
&mut self,
position: P,
) -> io::Result<()> {
let crate::layout::Position { x, y } = position.into();
self.buffered_terminal.add_change(Change::CursorPosition {
x: Position::Absolute(x as usize),
y: Position::Absolute(y as usize),

View file

@ -11,7 +11,7 @@ use unicode_width::UnicodeWidthStr;
use crate::{
backend::{Backend, ClearType, WindowSize},
buffer::{Buffer, Cell},
layout::{Rect, Size},
layout::{Position, Rect, Size},
};
/// A [`Backend`] implementation used for integration testing that renders to an memory buffer.
@ -117,6 +117,19 @@ impl TestBackend {
{
self.assert_buffer(&Buffer::with_lines(expected));
}
/// Asserts that the `TestBackend`'s cursor position is equal to the expected one.
///
/// This is a shortcut for `assert_eq!(self.get_cursor_position().unwrap(), expected)`.
///
/// # Panics
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual position.
#[track_caller]
pub fn assert_cursor_position<P: Into<Position>>(&mut self, position: P) {
let actual = self.get_cursor_position().unwrap();
assert_eq!(actual, position.into());
}
}
impl fmt::Display for TestBackend {
@ -148,12 +161,12 @@ impl Backend for TestBackend {
Ok(())
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
Ok(self.pos)
fn get_cursor_position(&mut self) -> io::Result<Position> {
Ok(self.pos.into())
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.pos = (x, y);
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
self.pos = position.into().into();
Ok(())
}
@ -203,7 +216,7 @@ impl Backend for TestBackend {
/// case but this limit is instead replaced with scrolling in most backend implementations) will
/// be added after the current position and the cursor will be moved to the last row.
fn append_lines(&mut self, n: u16) -> io::Result<()> {
let (cur_x, cur_y) = self.get_cursor()?;
let Position { x: cur_x, y: cur_y } = self.get_cursor_position()?;
let Rect { width, height, .. } = self.buffer.area;
// the next column ensuring that we don't go past the last column
@ -218,13 +231,13 @@ impl Backend for TestBackend {
self.clear()?;
}
self.set_cursor(0, rotate_by)?;
self.set_cursor_position(Position { x: 0, y: rotate_by })?;
self.clear_region(ClearType::BeforeCursor)?;
self.buffer.content.rotate_left((width * rotate_by).into());
}
let new_cursor_y = cur_y.saturating_add(n).min(max_y);
self.set_cursor(new_cursor_x, new_cursor_y)?;
self.set_cursor_position(Position::new(new_cursor_x, new_cursor_y))?;
Ok(())
}
@ -340,15 +353,23 @@ mod tests {
}
#[test]
fn get_cursor() {
fn get_cursor_position() {
let mut backend = TestBackend::new(10, 2);
assert_eq!(backend.get_cursor().unwrap(), (0, 0));
assert_eq!(backend.get_cursor_position().unwrap(), Position::ORIGIN);
}
#[test]
fn set_cursor() {
fn assert_cursor_position() {
let mut backend = TestBackend::new(10, 2);
backend.assert_cursor_position(Position::ORIGIN);
}
#[test]
fn set_cursor_position() {
let mut backend = TestBackend::new(10, 10);
backend.set_cursor(5, 5).unwrap();
backend
.set_cursor_position(Position { x: 5, y: 5 })
.unwrap();
assert_eq!(backend.pos, (5, 5));
}
@ -394,7 +415,9 @@ mod tests {
"aaaaaaaaaa",
]);
backend.set_cursor(3, 2).unwrap();
backend
.set_cursor_position(Position { x: 3, y: 2 })
.unwrap();
backend.clear_region(ClearType::AfterCursor).unwrap();
backend.assert_buffer_lines([
"aaaaaaaaaa",
@ -416,7 +439,9 @@ mod tests {
"aaaaaaaaaa",
]);
backend.set_cursor(5, 3).unwrap();
backend
.set_cursor_position(Position { x: 5, y: 3 })
.unwrap();
backend.clear_region(ClearType::BeforeCursor).unwrap();
backend.assert_buffer_lines([
" ",
@ -438,7 +463,9 @@ mod tests {
"aaaaaaaaaa",
]);
backend.set_cursor(3, 1).unwrap();
backend
.set_cursor_position(Position { x: 3, y: 1 })
.unwrap();
backend.clear_region(ClearType::CurrentLine).unwrap();
backend.assert_buffer_lines([
"aaaaaaaaaa",
@ -460,7 +487,9 @@ mod tests {
"aaaaaaaaaa",
]);
backend.set_cursor(3, 0).unwrap();
backend
.set_cursor_position(Position { x: 3, y: 0 })
.unwrap();
backend.clear_region(ClearType::UntilNewLine).unwrap();
backend.assert_buffer_lines([
"aaa ",
@ -482,22 +511,22 @@ mod tests {
"eeeeeeeeee",
]);
backend.set_cursor(0, 0).unwrap();
backend.set_cursor_position(Position::ORIGIN).unwrap();
// If the cursor is not at the last line in the terminal the addition of a
// newline simply moves the cursor down and to the right
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 1));
backend.assert_cursor_position(Position { x: 1, y: 1 });
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (2, 2));
backend.assert_cursor_position(Position { x: 2, y: 2 });
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (3, 3));
backend.assert_cursor_position(Position { x: 3, y: 3 });
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (4, 4));
backend.assert_cursor_position(Position { x: 4, y: 4 });
// As such the buffer should remain unchanged
backend.assert_buffer_lines([
@ -522,7 +551,9 @@ mod tests {
// If the cursor is at the last line in the terminal the addition of a
// newline will scroll the contents of the buffer
backend.set_cursor(0, 4).unwrap();
backend
.set_cursor_position(Position { x: 0, y: 4 })
.unwrap();
backend.append_lines(1).unwrap();
@ -536,7 +567,7 @@ mod tests {
// It also moves the cursor to the right, as is common of the behaviour of
// terminals in raw-mode
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_cursor_position(Position { x: 1, y: 4 });
}
#[test]
@ -550,13 +581,13 @@ mod tests {
"eeeeeeeeee",
]);
backend.set_cursor(0, 0).unwrap();
backend.set_cursor_position(Position::ORIGIN).unwrap();
// If the cursor is not at the last line in the terminal the addition of multiple
// newlines simply moves the cursor n lines down and to the right by 1
backend.append_lines(4).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_cursor_position(Position { x: 1, y: 4 });
// As such the buffer should remain unchanged
backend.assert_buffer_lines([
@ -579,10 +610,12 @@ mod tests {
"eeeeeeeeee",
]);
backend.set_cursor(0, 3).unwrap();
backend
.set_cursor_position(Position { x: 0, y: 3 })
.unwrap();
backend.append_lines(3).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_cursor_position(Position { x: 1, y: 4 });
backend.assert_buffer_lines([
"cccccccccc",
@ -604,10 +637,12 @@ mod tests {
"eeeeeeeeee",
]);
backend.set_cursor(0, 4).unwrap();
backend
.set_cursor_position(Position { x: 0, y: 4 })
.unwrap();
backend.append_lines(5).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_cursor_position(Position { x: 1, y: 4 });
backend.assert_buffer_lines([
" ",
@ -629,10 +664,10 @@ mod tests {
"eeeeeeeeee",
]);
backend.set_cursor(0, 0).unwrap();
backend.set_cursor_position(Position::ORIGIN).unwrap();
backend.append_lines(5).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_cursor_position(Position { x: 1, y: 4 });
backend.assert_buffer_lines([
"bbbbbbbbbb",

View file

@ -175,11 +175,22 @@ impl Frame<'_> {
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
/// with it.
/// Note that this will interfere with calls to [`Terminal::hide_cursor`],
/// [`Terminal::show_cursor`], and [`Terminal::set_cursor_position`]. Pick one of the APIs and
/// stick with it.
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) {
self.cursor_position = Some(position.into());
}
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
/// Note that this will interfere with calls to [`Terminal::hide_cursor`],
/// [`Terminal::show_cursor`], and [`Terminal::set_cursor_position`]. Pick one of the APIs and
/// stick with it.
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
pub fn set_cursor(&mut self, x: u16, y: u16) {
self.cursor_position = Some(Position { x, y });
self.set_cursor_position(Position { x, y });
}
/// Gets the buffer that this `Frame` draws into as a mutable reference.

View file

@ -28,16 +28,16 @@ use crate::{backend::ClearType, prelude::*, CompletedFrame, TerminalOptions, Vie
/// # Examples
///
/// ```rust,no_run
/// # use ratatui::prelude::*;
/// use std::io::stdout;
///
/// use ratatui::{prelude::*, widgets::Paragraph};
/// use ratatui::widgets::Paragraph;
///
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
/// terminal.draw(|frame| {
/// let area = frame.size();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor(0, 0);
/// })?;
/// # std::io::Result::Ok(())
/// ```
@ -275,17 +275,16 @@ where
/// # Examples
///
/// ```
/// # use ratatui::layout::Position;
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
/// # let mut terminal = ratatui::Terminal::new(backend)?;
/// use std::io;
///
/// use ratatui::widgets::Paragraph;
///
/// // with a closure
/// terminal.draw(|frame| {
/// let area = frame.size();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor(0, 0);
/// frame.set_cursor_position(Position { x: 0, y: 0 });
/// })?;
///
/// // or with a function
@ -294,7 +293,7 @@ where
/// fn render(frame: &mut ratatui::Frame) {
/// frame.render_widget(Paragraph::new("Hello World!"), frame.size());
/// }
/// # io::Result::Ok(())
/// # std::io::Result::Ok(())
/// ```
pub fn draw<F>(&mut self, render_callback: F) -> io::Result<CompletedFrame>
where
@ -345,6 +344,7 @@ where
/// # Examples
///
/// ```should_panic
/// # use ratatui::layout::Position;;
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
/// # let mut terminal = ratatui::Terminal::new(backend)?;
/// use std::io;
@ -356,7 +356,7 @@ where
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
/// let area = frame.size();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor(0, 0);
/// frame.set_cursor_position(Position { x: 0, y: 0 });
/// io::Result::Ok(())
/// })?;
///
@ -393,9 +393,9 @@ where
match cursor_position {
None => self.hide_cursor()?,
Some(Position { x, y }) => {
Some(position) => {
self.show_cursor()?;
self.set_cursor(x, y)?;
self.set_cursor_position(position)?;
}
}
@ -434,14 +434,30 @@ where
///
/// This is the position of the cursor after the last draw call and is returned as a tuple of
/// `(x, y)` coordinates.
#[deprecated = "the method get_cursor_position indicates more clearly what about the cursor to get"]
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
self.backend.get_cursor()
let Position { x, y } = self.get_cursor_position()?;
Ok((x, y))
}
/// Sets the cursor position.
#[deprecated = "the method aet_cursor_position indicates more clearly what about the cursor to set"]
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)?;
self.last_known_cursor_pos = Position { x, y };
self.set_cursor_position(Position { x, y })
}
/// Gets the current cursor position.
///
/// This is the position of the cursor after the last draw call.
pub fn get_cursor_position(&mut self) -> io::Result<Position> {
self.backend.get_cursor_position()
}
/// Sets the cursor position.
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
let position = position.into();
self.backend.set_cursor_position(position)?;
self.last_known_cursor_pos = position;
Ok(())
}
@ -451,12 +467,12 @@ where
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
Viewport::Inline(_) => {
self.backend
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
.set_cursor_position(self.viewport_area.as_position())?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
Viewport::Fixed(area) => {
for row in area.top()..area.bottom() {
self.backend.set_cursor(0, row)?;
for y in area.top()..area.bottom() {
self.backend.set_cursor_position(Position { x: 0, y })?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
}
@ -571,7 +587,7 @@ where
});
self.backend.draw(iter)?;
self.backend.flush()?;
self.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
self.set_cursor_position(self.viewport_area.as_position())?;
}
Ok(())
@ -584,7 +600,7 @@ fn compute_inline_size<B: Backend>(
size: Size,
offset_in_previous_viewport: u16,
) -> io::Result<(Rect, Position)> {
let pos: Position = backend.get_cursor()?.into();
let pos = backend.get_cursor_position()?;
let mut row = pos.y;
let max_height = size.height.min(height);