mirror of
https://github.com/wez/wezterm.git
synced 2024-12-24 13:52:55 +03:00
add theoretical support for storing Images in the Surface
There's basic rendering also, but it is not complete. Needs more tests; will come back to those once more scaffolding is in place.
This commit is contained in:
parent
5d27265b7c
commit
a267ebb576
@ -163,6 +163,11 @@ impl CellAttributes {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_image(&mut self, image: Option<Box<ImageCell>>) -> &mut Self {
|
||||
self.image = image;
|
||||
self
|
||||
}
|
||||
|
||||
/// Clone the attributes, but exclude fancy extras such
|
||||
/// as hyperlinks or future sprite things
|
||||
pub fn clone_sgr_only(&self) -> Self {
|
||||
|
@ -270,21 +270,21 @@ pub enum ITermProprietary {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ITermFileData {
|
||||
/// file name
|
||||
name: Option<String>,
|
||||
pub name: Option<String>,
|
||||
/// size of the data in bytes; this is used by iterm to show progress
|
||||
/// while waiting for the rest of the payload
|
||||
size: Option<u64>,
|
||||
pub size: Option<usize>,
|
||||
/// width to render
|
||||
width: ITermDimension,
|
||||
pub width: ITermDimension,
|
||||
/// height to render
|
||||
height: ITermDimension,
|
||||
pub height: ITermDimension,
|
||||
/// if true, preserve aspect ratio when fitting to width/height
|
||||
preserve_aspect_ratio: bool,
|
||||
pub preserve_aspect_ratio: bool,
|
||||
/// if true, attempt to display in the terminal rather than downloading to
|
||||
/// the users download directory
|
||||
inline: bool,
|
||||
pub inline: bool,
|
||||
/// The data to transfer
|
||||
data: Vec<u8>,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ITermFileData {
|
||||
|
@ -11,15 +11,25 @@
|
||||
// protocol appears to track the images out of band as attachments with
|
||||
// z-order.
|
||||
|
||||
use failure::Error;
|
||||
use image_crate::load_from_memory;
|
||||
use ordered_float::NotNaN;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TextureCoordinate {
|
||||
x: NotNaN<f32>,
|
||||
y: NotNaN<f32>,
|
||||
pub x: NotNaN<f32>,
|
||||
pub y: NotNaN<f32>,
|
||||
}
|
||||
|
||||
impl TextureCoordinate {
|
||||
pub fn new(x: NotNaN<f32>, y: NotNaN<f32>) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub fn new_f32(x: f32, y: f32) -> Self {
|
||||
let x = NotNaN::new(x).unwrap();
|
||||
let y = NotNaN::new(y).unwrap();
|
||||
Self::new(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks data for displaying an image in the place of the normal cell
|
||||
@ -39,39 +49,34 @@ pub struct ImageCell {
|
||||
data: Rc<ImageData>,
|
||||
}
|
||||
|
||||
impl ImageCell {
|
||||
pub fn new(
|
||||
top_left: TextureCoordinate,
|
||||
bottom_right: TextureCoordinate,
|
||||
data: Rc<ImageData>,
|
||||
) -> Self {
|
||||
Self {
|
||||
top_left,
|
||||
bottom_right,
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static IMAGE_ID: ::std::sync::atomic::AtomicUsize = ::std::sync::atomic::ATOMIC_USIZE_INIT;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ImageData {
|
||||
id: usize,
|
||||
/// Width of the image, in pixels
|
||||
width: usize,
|
||||
/// Height of the image, in pixels,
|
||||
height: usize,
|
||||
/// The image data bytes. Data is SRGBA, 32 bits per pixel
|
||||
/// The image data bytes. Data is the native image file format
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ImageData {
|
||||
/// Guess the image format from the contained buffer and return the
|
||||
/// decoded image data.
|
||||
pub fn load_from_memory(buffer: &[u8]) -> Result<ImageData, Error> {
|
||||
let img = load_from_memory(buffer)?.to_rgba();
|
||||
let width = img.width() as usize;
|
||||
let height = img.height() as usize;
|
||||
let data = img.into_raw();
|
||||
|
||||
Ok(Self::with_raw_data(width, height, data))
|
||||
}
|
||||
|
||||
pub fn with_raw_data(width: usize, height: usize, data: Vec<u8>) -> Self {
|
||||
/// Create a new ImageData struct with the provided raw data.
|
||||
pub fn with_raw_data(data: Vec<u8>) -> Self {
|
||||
let id = IMAGE_ID.fetch_add(1, ::std::sync::atomic::Ordering::Relaxed);
|
||||
Self {
|
||||
id,
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
}
|
||||
Self { id, data }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@ -83,14 +88,4 @@ impl ImageData {
|
||||
pub fn id(&self) -> usize {
|
||||
self.id
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,9 @@ use caps::{Capabilities, ColorLevel};
|
||||
use cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline};
|
||||
use color::{ColorAttribute, ColorSpec};
|
||||
use escape::csi::{Cursor, Edit, EraseInDisplay, EraseInLine, Sgr, CSI};
|
||||
use escape::osc::OperatingSystemCommand;
|
||||
use failure;
|
||||
use escape::osc::{ITermDimension, ITermFileData, ITermProprietary, OperatingSystemCommand};
|
||||
use failure::{self, Error};
|
||||
use image::TextureCoordinate;
|
||||
use std::io::{Read, Write};
|
||||
use surface::{Change, CursorShape, Position};
|
||||
use terminal::unix::UnixTty;
|
||||
@ -239,6 +240,40 @@ impl TerminfoRenderer {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cursor_up<W: UnixTty + Write>(&mut self, n: u32, out: &mut W) -> Result<(), Error> {
|
||||
if let Some(attr) = self.get_capability::<cap::ParmUpCursor>() {
|
||||
attr.expand().count(n).to(out.by_ref())?;
|
||||
} else {
|
||||
write!(out, "{}", CSI::Cursor(Cursor::Up(n)))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn cursor_down<W: UnixTty + Write>(&mut self, n: u32, out: &mut W) -> Result<(), Error> {
|
||||
if let Some(attr) = self.get_capability::<cap::ParmDownCursor>() {
|
||||
attr.expand().count(n).to(out.by_ref())?;
|
||||
} else {
|
||||
write!(out, "{}", CSI::Cursor(Cursor::Down(n)))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cursor_left<W: UnixTty + Write>(&mut self, n: u32, out: &mut W) -> Result<(), Error> {
|
||||
if let Some(attr) = self.get_capability::<cap::ParmLeftCursor>() {
|
||||
attr.expand().count(n).to(out.by_ref())?;
|
||||
} else {
|
||||
write!(out, "{}", CSI::Cursor(Cursor::Left(n)))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn cursor_right<W: UnixTty + Write>(&mut self, n: u32, out: &mut W) -> Result<(), Error> {
|
||||
if let Some(attr) = self.get_capability::<cap::ParmRightCursor>() {
|
||||
attr.expand().count(n).to(out.by_ref())?;
|
||||
} else {
|
||||
write!(out, "{}", CSI::Cursor(Cursor::Right(n)))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TerminfoRenderer {
|
||||
@ -412,43 +447,31 @@ impl TerminfoRenderer {
|
||||
}
|
||||
Change::CursorPosition {
|
||||
x: Position::NoChange,
|
||||
y: Position::Relative(1),
|
||||
} => {
|
||||
if let Some(attr) = self.get_capability::<cap::CursorDown>() {
|
||||
attr.expand().to(out.by_ref())?;
|
||||
} else {
|
||||
write!(out, "{}", CSI::Cursor(Cursor::Down(1)))?;
|
||||
}
|
||||
y: Position::Relative(n),
|
||||
} if *n > 0 =>
|
||||
{
|
||||
self.cursor_down(*n as u32, out)?;
|
||||
}
|
||||
Change::CursorPosition {
|
||||
x: Position::NoChange,
|
||||
y: Position::Relative(-1),
|
||||
} => {
|
||||
if let Some(attr) = self.get_capability::<cap::CursorUp>() {
|
||||
attr.expand().to(out.by_ref())?;
|
||||
} else {
|
||||
write!(out, "{}", CSI::Cursor(Cursor::Up(1)))?;
|
||||
}
|
||||
y: Position::Relative(n),
|
||||
} if *n < 0 =>
|
||||
{
|
||||
self.cursor_up(*n as u32, out)?;
|
||||
}
|
||||
Change::CursorPosition {
|
||||
x: Position::Relative(-1),
|
||||
x: Position::Relative(n),
|
||||
y: Position::NoChange,
|
||||
} => {
|
||||
if let Some(attr) = self.get_capability::<cap::CursorLeft>() {
|
||||
attr.expand().to(out.by_ref())?;
|
||||
} else {
|
||||
write!(out, "{}", CSI::Cursor(Cursor::Left(1)))?;
|
||||
}
|
||||
} if *n < 0 =>
|
||||
{
|
||||
self.cursor_left(*n as u32, out)?;
|
||||
}
|
||||
Change::CursorPosition {
|
||||
x: Position::Relative(1),
|
||||
x: Position::Relative(n),
|
||||
y: Position::NoChange,
|
||||
} => {
|
||||
if let Some(attr) = self.get_capability::<cap::CursorRight>() {
|
||||
attr.expand().to(out.by_ref())?;
|
||||
} else {
|
||||
write!(out, "{}", CSI::Cursor(Cursor::Right(1)))?;
|
||||
}
|
||||
} if *n > 0 =>
|
||||
{
|
||||
self.cursor_right(*n as u32, out)?;
|
||||
}
|
||||
Change::CursorPosition {
|
||||
x: Position::Absolute(x),
|
||||
@ -512,6 +535,52 @@ impl TerminfoRenderer {
|
||||
}
|
||||
}
|
||||
},
|
||||
Change::Image(image) => {
|
||||
if self.caps.iterm2_image() {
|
||||
let data = if image.top_left == TextureCoordinate::new_f32(0.0, 0.0)
|
||||
&& image.bottom_right == TextureCoordinate::new_f32(1.0, 1.0)
|
||||
{
|
||||
// The whole image is requested, so we can send the
|
||||
// original image bytes over
|
||||
image.image.data().to_vec()
|
||||
} else {
|
||||
// TODO: slice out the requested region of the image,
|
||||
// and encode as a PNG.
|
||||
unimplemented!();
|
||||
};
|
||||
|
||||
let file = ITermFileData {
|
||||
name: None,
|
||||
size: Some(data.len()),
|
||||
width: ITermDimension::Cells(image.width as i64),
|
||||
height: ITermDimension::Cells(image.height as i64),
|
||||
preserve_aspect_ratio: true,
|
||||
inline: true,
|
||||
data,
|
||||
};
|
||||
|
||||
let osc = OperatingSystemCommand::ITermProprietary(ITermProprietary::File(
|
||||
Box::new(file),
|
||||
));
|
||||
|
||||
write!(out, "{}", osc)?;
|
||||
|
||||
// TODO: } else if self.caps.sixel() {
|
||||
} else {
|
||||
// Blank out the cells and move the cursor to the right spot
|
||||
for y in 0..image.height {
|
||||
for _ in 0..image.width {
|
||||
write!(out, " ")?;
|
||||
}
|
||||
|
||||
if y != image.height - 1 {
|
||||
write!(out, "\n")?;
|
||||
self.cursor_left(image.width as u32, out)?;
|
||||
}
|
||||
}
|
||||
self.cursor_up(image.height as u32, out)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -262,6 +262,24 @@ impl WindowsConsoleRenderer {
|
||||
}
|
||||
Change::CursorColor(_color) => {}
|
||||
Change::CursorShape(_shape) => {}
|
||||
Change::Image(image) => {
|
||||
// Images are not supported, so just blank out the cells and
|
||||
// move the cursor to the right spot
|
||||
out.flush()?;
|
||||
let info = out.get_buffer_info()?;
|
||||
for y in 0..image.height {
|
||||
out.fill_char(
|
||||
' ',
|
||||
info.dwCursorPosition.X,
|
||||
y as i16 + info.dwCursorPosition.Y,
|
||||
image.width as u32,
|
||||
)?;
|
||||
}
|
||||
out.set_cursor_position(
|
||||
info.dwCursorPosition.X + image.width as i16,
|
||||
info.dwCursorPosition.Y,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
out.flush()?;
|
||||
|
@ -1,5 +1,7 @@
|
||||
use cell::{AttributeChange, CellAttributes};
|
||||
use color::ColorAttribute;
|
||||
pub use image::{ImageData, TextureCoordinate};
|
||||
use std::rc::Rc;
|
||||
use surface::{CursorShape, Position};
|
||||
|
||||
/// `Change` describes an update operation to be applied to a `Surface`.
|
||||
@ -40,6 +42,14 @@ pub enum Change {
|
||||
/// Change the cursor shape
|
||||
CursorShape(CursorShape),
|
||||
/* ChangeScrollRegion{top: usize, bottom: usize}, */
|
||||
/// Place an image at the current cursor position.
|
||||
/// The image defines the dimensions in cells.
|
||||
/// TODO: check iterm rendering behavior when the image is larger than the width of the screen.
|
||||
/// If the image is taller than the remaining space at the bottom
|
||||
/// of the screen, the screen will scroll up.
|
||||
/// The cursor Y position is unchanged by rendering the Image.
|
||||
/// The cursor X position will be incremented by `Image::width` cells.
|
||||
Image(Image),
|
||||
}
|
||||
|
||||
impl Change {
|
||||
@ -69,3 +79,29 @@ impl From<AttributeChange> for Change {
|
||||
Change::Attribute(c)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Image` `Change` needs to support adding an image that spans multiple
|
||||
/// rows and columns, as well as model the content for just one of those cells.
|
||||
/// For instance, if some of the cells inside an image are replaced by textual
|
||||
/// content, and the screen is scrolled, computing the diff change stream needs
|
||||
/// to be able to express that a single cell holds a slice from a larger image.
|
||||
/// The `Image` struct expresses its dimensions in cells and references a region
|
||||
/// in the shared source image data using texture coordinates.
|
||||
/// A 4x3 cell image would set `width=3`, `height=3`, `top_left=(0,0)`, `bottom_right=(1,1)`.
|
||||
/// The top left cell from that image, if it were to be included in a diff,
|
||||
/// would be recorded as `width=1`, `height=1`, `top_left=(0,0)`, `bottom_right=(1/4,1/3)`.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Image {
|
||||
/// measured in cells
|
||||
pub width: usize,
|
||||
/// measure in cells
|
||||
pub height: usize,
|
||||
/// Texture coordinate for the top left of this image block.
|
||||
/// (0,0) is the top left of the ImageData. (1, 1) is
|
||||
/// the bottom right.
|
||||
pub top_left: TextureCoordinate,
|
||||
/// Texture coordinates for the bottom right of this image block.
|
||||
pub bottom_right: TextureCoordinate,
|
||||
/// the image data
|
||||
pub image: Rc<ImageData>,
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
use cell::{AttributeChange, Cell, CellAttributes};
|
||||
use color::ColorAttribute;
|
||||
use image::ImageCell;
|
||||
use ordered_float::NotNaN;
|
||||
use std::borrow::Cow;
|
||||
use std::cmp::min;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
@ -7,7 +9,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
pub mod change;
|
||||
pub mod line;
|
||||
|
||||
pub use self::change::Change;
|
||||
pub use self::change::{Change, Image, TextureCoordinate};
|
||||
pub use self::line::Line;
|
||||
|
||||
/// Position holds 0-based positioning information, where
|
||||
@ -178,9 +180,55 @@ impl Surface {
|
||||
Change::ClearToEndOfScreen(color) => self.clear_eos(color),
|
||||
Change::CursorColor(color) => self.cursor_color = color.clone(),
|
||||
Change::CursorShape(shape) => self.cursor_shape = shape.clone(),
|
||||
Change::Image(image) => self.add_image(image),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_image(&mut self, image: &Image) {
|
||||
let xsize = (image.bottom_right.x - image.top_left.x) / image.width as f32;
|
||||
let ysize = (image.bottom_right.y - image.top_left.y) / image.height as f32;
|
||||
|
||||
if self.ypos + image.height > self.height {
|
||||
let scroll = (self.ypos + image.height) - self.height;
|
||||
for _ in 0..scroll {
|
||||
self.scroll_screen_up();
|
||||
}
|
||||
self.ypos -= scroll;
|
||||
}
|
||||
|
||||
let mut ypos = NotNaN::new(0.0).unwrap();
|
||||
for y in 0..image.height {
|
||||
let mut xpos = NotNaN::new(0.0).unwrap();
|
||||
for x in 0..image.width {
|
||||
self.lines[self.ypos + y].set_cell(
|
||||
self.xpos + x,
|
||||
Cell::new(
|
||||
' ',
|
||||
self.attributes
|
||||
.clone()
|
||||
.set_image(Some(Box::new(ImageCell::new(
|
||||
TextureCoordinate::new(
|
||||
image.top_left.x + xpos,
|
||||
image.top_left.y + ypos,
|
||||
),
|
||||
TextureCoordinate::new(
|
||||
image.top_left.x + xpos + xsize,
|
||||
image.top_left.y + ypos + ysize,
|
||||
),
|
||||
image.image.clone(),
|
||||
))))
|
||||
.clone(),
|
||||
),
|
||||
);
|
||||
|
||||
xpos += xsize;
|
||||
}
|
||||
ypos += ysize;
|
||||
}
|
||||
|
||||
self.xpos += image.width;
|
||||
}
|
||||
|
||||
fn clear_screen(&mut self, color: &ColorAttribute) {
|
||||
self.attributes = CellAttributes::default()
|
||||
.set_background(color.clone())
|
||||
@ -699,6 +747,8 @@ mod test {
|
||||
use super::*;
|
||||
use cell::Intensity;
|
||||
use color::AnsiColor;
|
||||
use image::ImageData;
|
||||
use std::rc::Rc;
|
||||
|
||||
// The \x20's look a little awkward, but we can't use a plain
|
||||
// space in the first chararcter of a multi-line continuation;
|
||||
@ -1356,4 +1406,136 @@ mod test {
|
||||
s.add_change("A\u{200b}B");
|
||||
assert_eq!(s.screen_chars_to_string(), "A\u{200b}B \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn images() {
|
||||
// a dummy image blob with nonsense content
|
||||
let data = Rc::new(ImageData::with_raw_data(0, 0, vec![]));
|
||||
let mut s = Surface::new(2, 2);
|
||||
s.add_change(Change::Image(Image {
|
||||
top_left: TextureCoordinate::new_f32(0.0, 0.0),
|
||||
bottom_right: TextureCoordinate::new_f32(1.0, 1.0),
|
||||
image: data.clone(),
|
||||
width: 4,
|
||||
height: 2,
|
||||
}));
|
||||
|
||||
// We're checking that we slice the image up and assign the correct
|
||||
// texture coordinates for each cell. The width and height are
|
||||
// different from each other to help ensure that the right terms
|
||||
// are used by add_image() function.
|
||||
assert_eq!(
|
||||
s.screen_cells(),
|
||||
[
|
||||
[
|
||||
Cell::new(
|
||||
' ',
|
||||
CellAttributes::default()
|
||||
.set_image(Some(Box::new(ImageCell::new(
|
||||
TextureCoordinate::new_f32(0.0, 0.0),
|
||||
TextureCoordinate::new_f32(0.25, 0.5),
|
||||
data.clone()
|
||||
))))
|
||||
.clone()
|
||||
),
|
||||
Cell::new(
|
||||
' ',
|
||||
CellAttributes::default()
|
||||
.set_image(Some(Box::new(ImageCell::new(
|
||||
TextureCoordinate::new_f32(0.25, 0.0),
|
||||
TextureCoordinate::new_f32(0.5, 0.5),
|
||||
data.clone()
|
||||
))))
|
||||
.clone()
|
||||
),
|
||||
Cell::new(
|
||||
' ',
|
||||
CellAttributes::default()
|
||||
.set_image(Some(Box::new(ImageCell::new(
|
||||
TextureCoordinate::new_f32(0.5, 0.0),
|
||||
TextureCoordinate::new_f32(0.75, 0.5),
|
||||
data.clone()
|
||||
))))
|
||||
.clone()
|
||||
),
|
||||
Cell::new(
|
||||
' ',
|
||||
CellAttributes::default()
|
||||
.set_image(Some(Box::new(ImageCell::new(
|
||||
TextureCoordinate::new_f32(0.75, 0.0),
|
||||
TextureCoordinate::new_f32(1.0, 0.5),
|
||||
data.clone()
|
||||
))))
|
||||
.clone()
|
||||
),
|
||||
],
|
||||
[
|
||||
Cell::new(
|
||||
' ',
|
||||
CellAttributes::default()
|
||||
.set_image(Some(Box::new(ImageCell::new(
|
||||
TextureCoordinate::new_f32(0.0, 0.5),
|
||||
TextureCoordinate::new_f32(0.25, 1.0),
|
||||
data.clone()
|
||||
))))
|
||||
.clone()
|
||||
),
|
||||
Cell::new(
|
||||
' ',
|
||||
CellAttributes::default()
|
||||
.set_image(Some(Box::new(ImageCell::new(
|
||||
TextureCoordinate::new_f32(0.25, 0.5),
|
||||
TextureCoordinate::new_f32(0.5, 1.0),
|
||||
data.clone()
|
||||
))))
|
||||
.clone()
|
||||
),
|
||||
Cell::new(
|
||||
' ',
|
||||
CellAttributes::default()
|
||||
.set_image(Some(Box::new(ImageCell::new(
|
||||
TextureCoordinate::new_f32(0.5, 0.5),
|
||||
TextureCoordinate::new_f32(0.75, 1.0),
|
||||
data.clone()
|
||||
))))
|
||||
.clone()
|
||||
),
|
||||
Cell::new(
|
||||
' ',
|
||||
CellAttributes::default()
|
||||
.set_image(Some(Box::new(ImageCell::new(
|
||||
TextureCoordinate::new_f32(0.75, 0.5),
|
||||
TextureCoordinate::new_f32(1.0, 1.0),
|
||||
data.clone()
|
||||
))))
|
||||
.clone()
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// Check that starting at not the texture origin coordinates
|
||||
// gives reasonable values in the resultant cell
|
||||
let mut other = Surface::new(1, 1);
|
||||
other.add_change(Change::Image(Image {
|
||||
top_left: TextureCoordinate::new_f32(0.25, 0.3),
|
||||
bottom_right: TextureCoordinate::new_f32(0.75, 0.8),
|
||||
image: data.clone(),
|
||||
width: 1,
|
||||
height: 1,
|
||||
}));
|
||||
assert_eq!(
|
||||
other.screen_cells(),
|
||||
[[Cell::new(
|
||||
' ',
|
||||
CellAttributes::default()
|
||||
.set_image(Some(Box::new(ImageCell::new(
|
||||
TextureCoordinate::new_f32(0.25, 0.3),
|
||||
TextureCoordinate::new_f32(0.75, 0.8),
|
||||
data.clone()
|
||||
))))
|
||||
.clone()
|
||||
),]]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user