2 个版本

0.1.1 2022年11月10日
0.1.0 2022年11月10日

#395 in GUI

MIT 许可证

515KB
11K SLoC

用于创建终端用户界面 (TUI) 的库

目标是通过提供足够的文档和许多精简的示例,使 TUI 创建变得快速简单,这样开发者就可以专注于应用程序的逻辑,而不是过多地考虑表示层的烦恼。

原因

受到 这篇帖子 的启发,我决定尝试创建一个用于终端用户界面管理的通用库,这是我用 Rust 编写的第一个更严肃的项目。

通过使您的程序更易于使用,您有更大的机会扩大您创建的实用程序的快乐用户群。

允许用户通过社区驱动的皮肤/主题和快捷键自定义这些解决方案的外观和感觉,这会使人们更有可能继续使用这些应用程序。

如何使用它

此库使用 termios 作为其唯一依赖项,这使得您可以在多个操作系统上使用它。

图形对象可以作为纯文本文件定义和加载。图形的构建块 - 框架也是文本文件,包含可选的 ANSI 转义码,允许使用颜色和不同的样式。

内部渲染由单独的线程提供服务,因此等待按键不会对动画播放产生影响。

附带的 studio 应用程序是此库有效工作的一个工作证明。它可以由非程序员使用,以便更注重设计的人可以帮助用户界面的外观和感觉。

设置

为了使用此库,您需要创建一个 Manager

use animaterm::prelude::*;
use std::time::Duration;

let capture_keyboard = true;
let cols = Some(40);
let rows = None;  // use all rows available
let glyph = Some(Glyph::default());  // initially fill the screen with this
// You can crank refresh_timeout down, but anything below 1ms won't make a difference,
// other than high CPU usage.
// With default 30ms you get as high as 33 FPS, probably enough for a terminal application.
let refresh_timeout = Some(Duration::from_milis(10));  
let mut mgr = Manager::new(capture_keyboard, cols, rows, glyph, refresh_timeout);

请注意,为了看到您使用此库所取得的进展,您需要保持您的程序运行,因为 animaterm 使用备用终端缓冲区。当您的程序完成时,您将回到原始缓冲区,清除迄今为止放置在屏幕上的所有图形。

为了保持您的程序运行,您可以使用以下循环

let mut keep_running = true;
 while keep_running {
    if let Some(key) = mgr.read_key() {
        match key {
            Key::Q | Key::ShiftQ => {
                keep_running = false;
            }
            _ => continue
        }
    }
}
mgr.terminate();

功能

在您的控制下,使用 mgr 您可以执行各种操作

示例

请查阅 examples 目录,了解如何使用此库的工作示例。

创建包含多个帧的 Graphic

let cols = 10;
let rows = 5;
let start_frame = 0;
let mut library = HashMap::with_capacity(2);

library.insert(
    start_frame,
    vec![Glyph::default(); rows * cols]);
library.insert(
    start_frame+1,
    vec![Glyph::plain(); rows * cols]);
 let animations = None;
 let mut gr = Graphic::new(cols, rows, start_frame, library, animations);

let layer = 0;
let offset = (15, 5);
let graphic_id = mgr.add_graphic(gr, layer, offset).unwrap();
mgr.set_graphic(graphic_id, start_frame);

向 Graphic 添加动画

// You can define some animations upon creation of a graphic:
let running = false;
let looping = true;
let seconds = 0;
let miliseconds = 500;
let ordering = vec![
    (start_frame, Timestamp::new(seconds, miliseconds)),
    (start_frame + 1, Timestamp::new(seconds, miliseconds)),
];
let start_time = Timestamp::now();
let animation = Animation::new(running, looping, ordering, start_time);
let mut animations = HashMap::new();
let anim_id = 0;
animations.insert(anim_id, animation);

let mut gr = Graphic::new(cols, rows, start_frame, library, Some(animations));

// Or add a new animation to existing graphic
let option_anim_id = gr.add_animation(Animation::new(running, looping, ordering, start_time));

// You can even create additional animation via a manager
mgr.add_animation(
    graphic_id,
    Animation::new(running, looping, ordering, Timestamp::now()),
);

let mut var_anim_id = None;
 if let Ok(AnimOk::AnimationAdded(anim_id)) = mgr.read_result() {
    var_anim_id = Some(anim_id);
}

根据按键进行操作

有关允许用户定义按键绑定的更灵活的解决方案,请查看 studio 如何实现用户输入循环。

let mut keep_running = true;
while keep_running {
    if let Some(key) = mgr.read_key() {
        match key {
            Key::Left => mgr.move_graphic(graphic_id, layer, (-1, 0)),
            Key::Right => mgr.move_graphic(graphic_id, layer, (1, 0)),
            Key::Up => mgr.move_graphic(graphic_id, layer, (0, -1)),
            Key::Down => mgr.move_graphic(graphic_id, layer, (0, 1)),
            Key::Q | Key::ShiftQ => {
                keep_running = false;
            }
            _ => continue,
        }
    }
}
 mgr.terminate();

将所选 Graphic 切换到不同的帧

// Use force wisely, since it causes entire screen to be refreshed,
// thus app is becoming less responsive.
let force = true;
mgr.set_graphic(graphic_id, frame_id, force)

将所选 Graphic 切换到不同的动画

mgr.start_animation(graphic_id, anim_id);

暂停所选动画

//You can pause immediately
mgr.pause_animation(graphic_id);

//You can pause on a selected frame
mgr.pause_animation_on_frame(graphic_id, frame_id);

在显示屏之间切换

let keep_existing = true;
let second_display_id = mgr.new_display(keep_existing);
// default display has id = 0
mgr.restore_display(0, keep_existing);

更改图形层

mgr.move_graphic(graphic_id, new_layer, (0, 0));

移动图形

mgr.move_graphic(graphic_id, layer, (offset_cols, offset_rows));

更新所选符号

// Change a Glyph for a selected Frame
a_graphic.set_frame(frame_id);
a_graphic.set_glyph(new_glyph, col, row, graphic_offset);

// Change a Glyph for current Frame of an on Screen Graphic
mgr.set_glyph(graphic_id, new_glyph, col, row);

使图形的一部分透明

// Make a transparent Glyph for a selected Frame
a_graphic.set_frame(frame_id, graphic_offset);
a_graphic.set_glyph(Glyph::transparent(), col, row, graphic_offset);

// Change a Glyph to transparent for current Frame of an on Screen Graphic
mgr.set_glyph(graphic_id, Glyph::transparent(), col, row, graphic_offset));

添加克隆帧

// In both cases empty Frame becomes current for that Graphic

// if source_frame_id is None current Frame will get cloned
let source_frame_id = Some(id);

// Add a new Frame to a graphic directly
let frame_id_option = a_graphic.clone_frame(source_frame_id);

// Or use a Manager to do so
mgr.clone_frame(graphic_id);
if let Ok(AnimOk::FrameAdded(graphic_id, frame_id)) = mgr.read_result() {
    let added_frame_id = frame_id;
}

添加新帧

// In both cases empty Frame becomes current for that Graphic

// Add a new Frame to a graphic directly
let frame_id_option = a_graphic.empty_frame();

// Or use a Manager to do so
mgr.empty_frame(graphic_id);
if let Ok(AnimOk::FrameAdded(graphic_id, frame_id)) = mgr.read_result() {
    let added_frame_id = frame_id;
}

依赖项