better tick system

This commit is contained in:
2024-11-02 23:49:48 +01:00
parent 14261ab1e1
commit 7f5a8d9b0a
6 changed files with 96 additions and 385 deletions
+61 -27
View File
@@ -9,13 +9,11 @@ pub trait Element: std::fmt::Debug {
fn get_child(&mut self) -> Option<&mut Box<dyn Element>>; fn get_child(&mut self) -> Option<&mut Box<dyn Element>>;
fn get_data(&self) -> ElementData; fn get_data(&self) -> ElementData;
fn set_data(&mut 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() String::new()
} }
fn update(&mut self, _: crate::key::Key) -> UpdateResponse { fn update(&mut self, _ctx: &mut UpdateContext) {}
UpdateResponse::None
}
fn tick(&mut self, _: usize) {}
} }
#[derive(Debug)] #[derive(Debug)]
@@ -32,7 +30,12 @@ pub enum UpdateResponse {
Exit, Exit,
Done, Done,
None, None,
Tick(Vec<u32>), }
pub struct UpdateContext {
pub key: key::Key,
pub tick: usize,
pub response: UpdateResponse,
} }
pub struct App { 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 /// Sets a component
pub fn set_component(&mut self, element: Box<dyn Element>) { pub fn set_component(&mut self, element: Box<dyn Element>) {
let (width, height) = crossterm::terminal::size().unwrap(); let (width, height) = crossterm::terminal::size().unwrap();
@@ -63,7 +73,7 @@ impl App {
} }
/// Render to the screen /// Render to the screen
pub fn render(&mut self) { fn render(&mut self, tick: usize) {
let (width, height) = crossterm::terminal::size().unwrap(); let (width, height) = crossterm::terminal::size().unwrap();
let mut data = self.element.get_data(); let mut data = self.element.get_data();
if data.width == 0 { if data.width == 0 {
@@ -74,7 +84,7 @@ impl App {
} }
self.element.set_data(data); self.element.set_data(data);
let mut frame: Vec<String> = create_frame!(width as usize, height as usize); 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(); utils::clear();
print!("{}", frame.join("")); print!("{}", frame.join(""));
utils::flush(); utils::flush();
@@ -88,18 +98,9 @@ impl App {
self.element.set_data(data); self.element.set_data(data);
} }
/// Run the screen fn update(&mut self, ctx: &mut UpdateContext) {
pub fn run(&mut self) { self.element.update(ctx);
utils::hide_cursor(); match ctx.response {
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()) {
UpdateResponse::Exit => { UpdateResponse::Exit => {
crossterm::terminal::disable_raw_mode().unwrap(); crossterm::terminal::disable_raw_mode().unwrap();
utils::clear(); utils::clear();
@@ -107,15 +108,48 @@ impl App {
println!(""); println!("");
return; 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")
}
}
}
} }
} }
+17
View File
@@ -160,6 +160,7 @@ macro_rules! element {
pub child: usize, pub child: usize,
pub text: String, pub text: String,
pub style: Style, pub style: Style,
pub tick_line: std::collections::HashMap<usize, String>,
$( $inner )* $( $inner )*
} }
@@ -184,6 +185,10 @@ macro_rules! element {
self.height = data.height; self.height = data.height;
self.style = data.style; self.style = data.style;
} }
fn clear_ticks(&mut self) {
self.tick_line.clear();
}
$( $functions )* $( $functions )*
} }
@@ -198,9 +203,21 @@ macro_rules! element {
child: 0, child: 0,
text: String::new(), text: String::new(),
style: Style::default(), style: Style::default(),
tick_line: std::collections::HashMap::new(),
$( $defaults )* $( $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
View File
@@ -1,53 +1,9 @@
use osui::{key::KeyKind, oml, ui::*, App}; use osui::{oml, ui::*, App};
fn main() { fn main() {
let mut app_screen = App::new(); let mut app_screen = App::new();
app_screen.set_component(oml!( app_screen.set_component(oml!(
button("okay";)
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
})
}
}
)); ));
app_screen.run(); app_screen.run();
} }
+17 -267
View File
@@ -1,146 +1,48 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::{ use crate::{
create_frame, element, element,
key::{Key, KeyKind}, key::KeyKind,
ui::Style, ui::Style,
Element, ElementData, UpdateResponse, Element, ElementData, UpdateContext,
}; };
element! { element! {
Text {} Text {}
defaults {} defaults {}
fn render(&mut self) -> String { fn render(&mut self, _: usize) -> String {
self.style.write(&self.text) 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! { element! {
Button { Button {
pub binds: HashMap<KeyKind, String>, pub binds: HashMap<KeyKind, String>,
pub toggle: bool,
pub on_click: fn(&mut Button), pub on_click: fn(&mut Button),
clicked: bool, clicked: bool
} }
defaults { defaults {
binds: HashMap::from([(KeyKind::Enter, String::from("click"))]), binds: HashMap::from([(KeyKind::Enter, String::from("click"))]),
toggle: false,
clicked: false,
on_click: |_|{}, 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 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.clicked = true;
self.add_action(ctx.tick+5, "un_click");
(self.on_click)(self); (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 { if self.clicked {
return self.style.write_clicked(&self.text); 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
View File
@@ -16,19 +16,6 @@ pub fn text() -> Box<Text> {
Box::new(Text::new()) 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. /// Creates and returns a `Box` containing a new `Button` element.
/// ///
/// This function constructs a `Button` element and wraps it in a `Box` to /// 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. /// A boxed `Button` element, initialized and ready for configuration.
pub fn button() -> Box<Button> { pub fn button() -> Box<Button> {
Box::new(Button::new()) let mut btn = Button::new();
} btn.style.clicked_bg = styles::Color::Red;
Box::new(btn)
/// 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)
} }
+2 -2
View File
@@ -82,9 +82,9 @@ fn merge_line(frame_: &str, line_: &str, x: usize) -> String {
} }
/// Render to a frame /// 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(); 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() { if (data.y + i) < frame.len() {
let frame_line = frame.get_mut(data.y + i).unwrap(); let frame_line = frame.get_mut(data.y + i).unwrap();
*frame_line = merge_line(&frame_line, line, data.x); *frame_line = merge_line(&frame_line, line, data.x);