better tick system
This commit is contained in:
+61
-27
@@ -9,13 +9,11 @@ pub trait Element: std::fmt::Debug {
|
||||
fn get_child(&mut self) -> Option<&mut Box<dyn Element>>;
|
||||
fn get_data(&self) -> ElementData;
|
||||
fn set_data(&mut self, _: ElementData);
|
||||
fn render(&mut self) -> String {
|
||||
fn clear_ticks(&mut self);
|
||||
fn render(&mut self, _tick: usize) -> String {
|
||||
String::new()
|
||||
}
|
||||
fn update(&mut self, _: crate::key::Key) -> UpdateResponse {
|
||||
UpdateResponse::None
|
||||
}
|
||||
fn tick(&mut self, _: usize) {}
|
||||
fn update(&mut self, _ctx: &mut UpdateContext) {}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -32,7 +30,12 @@ pub enum UpdateResponse {
|
||||
Exit,
|
||||
Done,
|
||||
None,
|
||||
Tick(Vec<u32>),
|
||||
}
|
||||
|
||||
pub struct UpdateContext {
|
||||
pub key: key::Key,
|
||||
pub tick: usize,
|
||||
pub response: UpdateResponse,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
@@ -47,6 +50,13 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new screen to render components with a pre-existing component
|
||||
pub fn from(elem: Box<dyn Element>) -> App {
|
||||
let mut app = App::new();
|
||||
app.set_component(elem);
|
||||
app
|
||||
}
|
||||
|
||||
/// Sets a component
|
||||
pub fn set_component(&mut self, element: Box<dyn Element>) {
|
||||
let (width, height) = crossterm::terminal::size().unwrap();
|
||||
@@ -63,7 +73,7 @@ impl App {
|
||||
}
|
||||
|
||||
/// Render to the screen
|
||||
pub fn render(&mut self) {
|
||||
fn render(&mut self, tick: usize) {
|
||||
let (width, height) = crossterm::terminal::size().unwrap();
|
||||
let mut data = self.element.get_data();
|
||||
if data.width == 0 {
|
||||
@@ -74,7 +84,7 @@ impl App {
|
||||
}
|
||||
self.element.set_data(data);
|
||||
let mut frame: Vec<String> = create_frame!(width as usize, height as usize);
|
||||
utils::render_to_frame(&mut frame, &mut self.element);
|
||||
utils::render_to_frame(tick, &mut frame, &mut self.element);
|
||||
utils::clear();
|
||||
print!("{}", frame.join(""));
|
||||
utils::flush();
|
||||
@@ -88,18 +98,9 @@ impl App {
|
||||
self.element.set_data(data);
|
||||
}
|
||||
|
||||
/// Run the screen
|
||||
pub fn run(&mut self) {
|
||||
utils::hide_cursor();
|
||||
utils::clear();
|
||||
let mut stdout = std::io::stdout();
|
||||
stdout
|
||||
.execute(crossterm::terminal::EnterAlternateScreen)
|
||||
.unwrap();
|
||||
crossterm::terminal::enable_raw_mode().unwrap();
|
||||
loop {
|
||||
self.render();
|
||||
match self.element.update(key::read_key()) {
|
||||
fn update(&mut self, ctx: &mut UpdateContext) {
|
||||
self.element.update(ctx);
|
||||
match ctx.response {
|
||||
UpdateResponse::Exit => {
|
||||
crossterm::terminal::disable_raw_mode().unwrap();
|
||||
utils::clear();
|
||||
@@ -107,15 +108,48 @@ impl App {
|
||||
println!("");
|
||||
return;
|
||||
}
|
||||
UpdateResponse::Tick(ticks) => {
|
||||
for (i, n) in ticks.into_iter().enumerate() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(n as u64));
|
||||
self.element.tick(i);
|
||||
self.render();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the screen
|
||||
pub fn run(&mut self) {
|
||||
// Initialize
|
||||
utils::hide_cursor();
|
||||
utils::clear();
|
||||
let mut stdout = std::io::stdout();
|
||||
stdout
|
||||
.execute(crossterm::terminal::EnterAlternateScreen)
|
||||
.unwrap();
|
||||
crossterm::terminal::enable_raw_mode().unwrap();
|
||||
|
||||
// Start the update thread
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || loop {
|
||||
tx.send(key::read_key()).unwrap();
|
||||
});
|
||||
|
||||
// Start the render loop
|
||||
let mut tick: usize = 0;
|
||||
loop {
|
||||
if tick > 99 {
|
||||
tick = 0;
|
||||
}
|
||||
self.render(tick);
|
||||
tick += 1;
|
||||
match rx.try_recv() {
|
||||
Ok(k) => self.update(&mut UpdateContext {
|
||||
key: k,
|
||||
tick,
|
||||
response: UpdateResponse::None,
|
||||
}),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => {
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||
panic!("disconnected")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ macro_rules! element {
|
||||
pub child: usize,
|
||||
pub text: String,
|
||||
pub style: Style,
|
||||
pub tick_line: std::collections::HashMap<usize, String>,
|
||||
$( $inner )*
|
||||
}
|
||||
|
||||
@@ -184,6 +185,10 @@ macro_rules! element {
|
||||
self.height = data.height;
|
||||
self.style = data.style;
|
||||
}
|
||||
|
||||
fn clear_ticks(&mut self) {
|
||||
self.tick_line.clear();
|
||||
}
|
||||
$( $functions )*
|
||||
}
|
||||
|
||||
@@ -198,9 +203,21 @@ macro_rules! element {
|
||||
child: 0,
|
||||
text: String::new(),
|
||||
style: Style::default(),
|
||||
tick_line: std::collections::HashMap::new(),
|
||||
$( $defaults )*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_action(&self, tick: usize) -> String {
|
||||
match self.tick_line.get(&tick) {
|
||||
Some(action) => action.clone(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_action(&mut self, tick: usize, action: &str) {
|
||||
self.tick_line.insert(tick, action.to_string());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+2
-46
@@ -1,53 +1,9 @@
|
||||
use osui::{key::KeyKind, oml, ui::*, App};
|
||||
use osui::{oml, ui::*, App};
|
||||
|
||||
fn main() {
|
||||
let mut app_screen = App::new();
|
||||
app_screen.set_component(oml!(
|
||||
|
||||
tab {
|
||||
div {
|
||||
// Some welcome text
|
||||
text("Welcome! This is the OSUI Tutorial! Press tab to go to the next Page or press Shift+Tab to go to a previous page";)
|
||||
}
|
||||
|
||||
// A button example With a div
|
||||
|
||||
div {
|
||||
text("On the bottom there is a button. To hover it click the down key, Then press enter to select it";)
|
||||
|
||||
// Button
|
||||
button("Click me!"; y = 3, style = Style {
|
||||
hover_fg: Color::Magenta,
|
||||
..Default::default()
|
||||
}, on_click = |btn| {
|
||||
btn.text = "Press tab to go to the next page".to_string();
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
div {
|
||||
text("This is a menu, ";)
|
||||
|
||||
// Button
|
||||
menu(y = 2, items = vec![
|
||||
String::from("Item 1"),
|
||||
String::from("Item 2"),
|
||||
String::from("Item 3"),
|
||||
String::from("Item 4"),
|
||||
String::from("Item 5"),
|
||||
String::from("Item 6"),
|
||||
], on_click = |m, k| {
|
||||
if k.kind == KeyKind::Escape {
|
||||
return ClickResponse::Exit;
|
||||
} else if k.kind == KeyKind::Enter {
|
||||
m.items[m.selected] = "Press Tab to go to the next page".to_string();
|
||||
}
|
||||
ClickResponse::None
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
button("okay";)
|
||||
));
|
||||
app_screen.run();
|
||||
}
|
||||
|
||||
+17
-267
@@ -1,146 +1,48 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
create_frame, element,
|
||||
key::{Key, KeyKind},
|
||||
element,
|
||||
key::KeyKind,
|
||||
ui::Style,
|
||||
Element, ElementData, UpdateResponse,
|
||||
Element, ElementData, UpdateContext,
|
||||
};
|
||||
|
||||
element! {
|
||||
Text {}
|
||||
defaults {}
|
||||
fn render(&mut self) -> String {
|
||||
fn render(&mut self, _: usize) -> String {
|
||||
self.style.write(&self.text)
|
||||
}
|
||||
}
|
||||
|
||||
element! {
|
||||
Div {
|
||||
pub binds: HashMap<KeyKind, String>
|
||||
}
|
||||
defaults {
|
||||
binds: HashMap::from([
|
||||
(KeyKind::Up, String::from("up")),
|
||||
(KeyKind::Down, String::from("down")),
|
||||
(KeyKind::Left, String::from("left")),
|
||||
(KeyKind::Right, String::from("right")),
|
||||
]),
|
||||
}
|
||||
fn update(&mut self, k: Key) -> UpdateResponse {
|
||||
if let Some(v) = self.binds.get(&k.kind) {
|
||||
match v.as_str() {
|
||||
"up" => {
|
||||
self.child =
|
||||
crate::utils::closest_component(&self.children, self.child, crate::utils::Direction::Up);
|
||||
}
|
||||
|
||||
"down" => {
|
||||
self.child = crate::utils::closest_component(
|
||||
&self.children,
|
||||
self.child,
|
||||
crate::utils::Direction::Down,
|
||||
);
|
||||
}
|
||||
|
||||
"left" => {
|
||||
self.child = crate::utils::closest_component(
|
||||
&self.children,
|
||||
self.child,
|
||||
crate::utils::Direction::Left,
|
||||
);
|
||||
}
|
||||
|
||||
"right" => {
|
||||
self.child = crate::utils::closest_component(
|
||||
&self.children,
|
||||
self.child,
|
||||
crate::utils::Direction::Right,
|
||||
);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
if let Some(child) = self.get_child() {
|
||||
return child.update(k);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateResponse::None
|
||||
}
|
||||
|
||||
fn render(&mut self) -> String {
|
||||
let mut frame: Vec<String> = create_frame!(self.width, self.height);
|
||||
for (i, child) in &mut self.children.iter_mut().enumerate() {
|
||||
let mut data = child.get_data();
|
||||
if data.width == 0 {
|
||||
data.width = self.width;
|
||||
}
|
||||
if data.height == 0 {
|
||||
data.height = self.height;
|
||||
}
|
||||
data.style.is_active = self.style.is_active && i == self.child;
|
||||
child.set_data(data);
|
||||
crate::utils::render_to_frame(&mut frame, child);
|
||||
let mut data = child.get_data();
|
||||
if data.width == self.width {
|
||||
data.width = 0;
|
||||
}
|
||||
if data.height == self.height {
|
||||
data.height = 0;
|
||||
}
|
||||
child.set_data(data);
|
||||
}
|
||||
frame.join("\n")
|
||||
}
|
||||
|
||||
fn tick(&mut self, i: usize) {
|
||||
if let Some(child) = self.get_child() {
|
||||
return child.tick(i);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
element! {
|
||||
Button {
|
||||
pub binds: HashMap<KeyKind, String>,
|
||||
pub toggle: bool,
|
||||
pub on_click: fn(&mut Button),
|
||||
clicked: bool,
|
||||
clicked: bool
|
||||
}
|
||||
defaults {
|
||||
binds: HashMap::from([(KeyKind::Enter, String::from("click"))]),
|
||||
toggle: false,
|
||||
clicked: false,
|
||||
on_click: |_|{},
|
||||
clicked: false,
|
||||
}
|
||||
fn update(&mut self, k: Key) -> UpdateResponse {
|
||||
if let Some(v) = self.binds.get(&k.kind) {
|
||||
|
||||
fn update(&mut self, ctx: &mut UpdateContext) {
|
||||
if let Some(v) = self.binds.get(&ctx.key.kind) {
|
||||
if v == "click" {
|
||||
if self.toggle {
|
||||
self.clicked = !self.clicked;
|
||||
(self.on_click)(self);
|
||||
} else {
|
||||
return UpdateResponse::Tick(vec![1, 100]);
|
||||
}
|
||||
}
|
||||
}
|
||||
UpdateResponse::None
|
||||
}
|
||||
|
||||
fn tick(&mut self, i: usize) {
|
||||
if i == 0 {
|
||||
self.clicked = true;
|
||||
self.add_action(ctx.tick+5, "un_click");
|
||||
(self.on_click)(self);
|
||||
} else if i == 1 {
|
||||
self.clicked = false;
|
||||
(self.on_click)(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self) -> String {
|
||||
fn render(&mut self, tick: usize) -> String {
|
||||
|
||||
if self.get_action(tick)=="un_click" {
|
||||
self.clicked = false;
|
||||
}
|
||||
|
||||
if self.clicked {
|
||||
return self.style.write_clicked(&self.text);
|
||||
}
|
||||
@@ -148,155 +50,3 @@ element! {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
element! {
|
||||
Tab {
|
||||
pub binds: HashMap<KeyKind, String>,
|
||||
}
|
||||
defaults {
|
||||
binds: HashMap::from([
|
||||
(KeyKind::Tab, String::from("next")),
|
||||
(KeyKind::ShiftTab, String::from("previous")),
|
||||
]),
|
||||
}
|
||||
fn update(&mut self, k: Key) -> UpdateResponse {
|
||||
if let Some(v) = self.binds.get(&k.kind) {
|
||||
match v.as_str() {
|
||||
"next" => {
|
||||
if self.child + 1 < self.children.len() {
|
||||
self.child += 1;
|
||||
} else {
|
||||
self.child = 0;
|
||||
}
|
||||
}
|
||||
"previous" => {
|
||||
if self.child == 0 {
|
||||
self.child = self.children.len() - 1;
|
||||
} else {
|
||||
self.child -= 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
if let Some(child) = self.get_child() {
|
||||
return child.update(k);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
UpdateResponse::None
|
||||
}
|
||||
|
||||
fn render(&mut self) -> String {
|
||||
let mut frame: Vec<String> = create_frame!(self.width, self.height - 1);
|
||||
let this = self.get_data();
|
||||
if let Some(child) = self.get_child() {
|
||||
let mut data = child.get_data();
|
||||
if data.width == 0 {
|
||||
data.width = this.width;
|
||||
}
|
||||
if data.height == 0 {
|
||||
data.height = this.height;
|
||||
}
|
||||
data.style.is_active = true;
|
||||
child.set_data(data);
|
||||
crate::utils::render_to_frame(&mut frame, child);
|
||||
let mut data = child.get_data();
|
||||
if data.width == this.width {
|
||||
data.width = 0;
|
||||
}
|
||||
if data.height == this.height {
|
||||
data.height = 0;
|
||||
}
|
||||
child.set_data(data);
|
||||
}
|
||||
let mut v: String = " ".repeat((self.width / 2) - self.children.len());
|
||||
for (i, _) in (&self.children).into_iter().enumerate() {
|
||||
if i == self.child {
|
||||
v += self.style.write_selected("*").as_str()
|
||||
} else {
|
||||
v += self.style.write("*").as_str()
|
||||
}
|
||||
}
|
||||
format!("{}\n{}", v, frame.join("\n"))
|
||||
}
|
||||
|
||||
fn tick(&mut self, i: usize) {
|
||||
if let Some(child) = self.get_child() {
|
||||
return child.tick(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The response of a on_click function in a element
|
||||
pub enum ClickResponse {
|
||||
None,
|
||||
Exit,
|
||||
}
|
||||
|
||||
element! {
|
||||
Menu {
|
||||
pub items: Vec<String>,
|
||||
pub selected: usize,
|
||||
pub on_click: fn(&mut Menu, _: Key) -> ClickResponse,
|
||||
pub binds: HashMap<KeyKind, String>,
|
||||
}
|
||||
defaults {
|
||||
items: Vec::new(),
|
||||
selected: 0,
|
||||
on_click: |_, _|ClickResponse::None,
|
||||
binds: HashMap::from([
|
||||
(KeyKind::Char(String::from("w")), String::from("up")),
|
||||
(KeyKind::Char(String::from("s")), String::from("down"))
|
||||
])
|
||||
}
|
||||
fn render(&mut self) -> String {
|
||||
let mut res: Vec<String> = Vec::new();
|
||||
|
||||
for (i, item) in self.items.iter_mut().enumerate() {
|
||||
if i == self.selected {
|
||||
res.push(format!(
|
||||
"{}{}",
|
||||
self.style.write_cursor("> "),
|
||||
self.style.write_selected(&item)
|
||||
));
|
||||
} else {
|
||||
res.push(format!(" {}", item));
|
||||
}
|
||||
}
|
||||
res.join(&self.style.write("\n"))
|
||||
}
|
||||
|
||||
fn update(&mut self, k: Key) -> UpdateResponse {
|
||||
if let Some(c) = self.binds.get(&k.kind) {
|
||||
match c.as_str() {
|
||||
"up" => {
|
||||
if self.selected > 0 {
|
||||
self.selected -= 1;
|
||||
} else {
|
||||
self.selected = self.items.len() - 1;
|
||||
}
|
||||
}
|
||||
"down" => {
|
||||
if self.items.len() > self.selected + 1 {
|
||||
self.selected += 1;
|
||||
} else {
|
||||
self.selected = 0;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return match (self.on_click)(self, k) {
|
||||
ClickResponse::Exit => UpdateResponse::Exit,
|
||||
ClickResponse::None => UpdateResponse::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return match (self.on_click)(self, k) {
|
||||
ClickResponse::Exit => UpdateResponse::Exit,
|
||||
ClickResponse::None => UpdateResponse::None,
|
||||
}
|
||||
}
|
||||
UpdateResponse::None
|
||||
}
|
||||
}
|
||||
|
||||
+3
-49
@@ -16,19 +16,6 @@ pub fn text() -> Box<Text> {
|
||||
Box::new(Text::new())
|
||||
}
|
||||
|
||||
/// Creates and returns a `Box` containing a new `Div` element.
|
||||
///
|
||||
/// This function constructs a `Div` element and wraps it in a `Box` to
|
||||
/// manage heap allocation and ownership. A `Div` can be used as a container
|
||||
/// for other elements or to organize layout.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A boxed `Div` element, initialized and ready for layout management.
|
||||
pub fn div() -> Box<Div> {
|
||||
Box::new(Div::new())
|
||||
}
|
||||
|
||||
/// Creates and returns a `Box` containing a new `Button` element.
|
||||
///
|
||||
/// This function constructs a `Button` element and wraps it in a `Box` to
|
||||
@@ -39,40 +26,7 @@ pub fn div() -> Box<Div> {
|
||||
///
|
||||
/// A boxed `Button` element, initialized and ready for configuration.
|
||||
pub fn button() -> Box<Button> {
|
||||
Box::new(Button::new())
|
||||
}
|
||||
|
||||
/// Creates and returns a `Box` containing a new `Tab` element with custom styling.
|
||||
///
|
||||
/// This function constructs a `Tab` element, sets the foreground color
|
||||
/// of the selected tab to red, and wraps the element in a `Box` to manage
|
||||
/// heap allocation and ownership.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A boxed `Tab` element, initialized and styled with a red foreground
|
||||
/// color for the selected tab.
|
||||
pub fn tab() -> Box<Tab> {
|
||||
let mut elem: Tab = Tab::new();
|
||||
elem.style.selected_fg = Color::Red;
|
||||
Box::new(elem)
|
||||
}
|
||||
|
||||
/// Creates and returns a `Box` containing a new `Menu` element with custom styling.
|
||||
///
|
||||
/// This function constructs a `Menu` element, sets the background and
|
||||
/// foreground colors of the selected menu item, and defines the cursor
|
||||
/// color. The element is wrapped in a `Box` for heap allocation and
|
||||
/// ownership management.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A boxed `Menu` element, initialized and styled with a magenta background
|
||||
/// and black foreground for selected items, and a magenta cursor color.
|
||||
pub fn menu() -> Box<Menu> {
|
||||
let mut elem = Menu::new();
|
||||
elem.style.selected_bg = Color::Magenta;
|
||||
elem.style.selected_fg = Color::Black;
|
||||
elem.style.cursor_fg = Color::Magenta;
|
||||
Box::new(elem)
|
||||
let mut btn = Button::new();
|
||||
btn.style.clicked_bg = styles::Color::Red;
|
||||
Box::new(btn)
|
||||
}
|
||||
|
||||
+2
-2
@@ -82,9 +82,9 @@ fn merge_line(frame_: &str, line_: &str, x: usize) -> String {
|
||||
}
|
||||
|
||||
/// Render to a frame
|
||||
pub fn render_to_frame(frame: &mut Vec<String>, element: &mut Box<dyn Element>) {
|
||||
pub fn render_to_frame(tick: usize, frame: &mut Vec<String>, element: &mut Box<dyn Element>) {
|
||||
let data = element.get_data();
|
||||
for (i, line) in element.render().split('\n').enumerate() {
|
||||
for (i, line) in element.render(tick).split('\n').enumerate() {
|
||||
if (data.y + i) < frame.len() {
|
||||
let frame_line = frame.get_mut(data.y + i).unwrap();
|
||||
*frame_line = merge_line(&frame_line, line, data.x);
|
||||
|
||||
Reference in New Issue
Block a user