added scrolling to widgets
This commit is contained in:
parent
3a5d8e929d
commit
dafd1493fe
|
@ -248,8 +248,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "termion"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "659c1f379f3408c7e5e84c7d0da6d93404e3800b6b9d063ba24436419302ec90"
|
||||
source = "git+https://sectorinf.com/emilis/termion#ce611b828393cf1bad1667089af9b41a7d99a768"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"numtoa",
|
||||
|
|
|
@ -7,8 +7,11 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
anyhow = "1.0.68"
|
||||
termion = "2.0.1"
|
||||
# termion = "2.0.1"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["std", "derive"]
|
||||
|
||||
[dependencies.termion]
|
||||
git = "https://sectorinf.com/emilis/termion"
|
|
@ -85,21 +85,16 @@ impl PartialEq for Component {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Line {
|
||||
color: ColorSet,
|
||||
components: Vec<Component>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq)]
|
||||
pub struct Widget {
|
||||
want_width: u8,
|
||||
width_pct: u8,
|
||||
per_line: Vec<Token>,
|
||||
scroll_offset: Option<usize>,
|
||||
}
|
||||
|
||||
impl PartialEq for Widget {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.want_width == other.want_width
|
||||
self.width_pct == other.width_pct
|
||||
&& self.per_line.len() == other.per_line.len()
|
||||
&& (&self.per_line)
|
||||
.into_iter()
|
||||
|
@ -111,13 +106,64 @@ impl PartialEq for Widget {
|
|||
impl Widget {
|
||||
pub fn new(width_pct: u8, tokens_per_line: Vec<Token>) -> Self {
|
||||
Self {
|
||||
want_width: width_pct,
|
||||
width_pct,
|
||||
per_line: tokens_per_line,
|
||||
scroll_offset: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_line(&self, line: usize) -> Option<&Token> {
|
||||
self.per_line.get(line)
|
||||
pub fn scrolling(
|
||||
width_pct: u8,
|
||||
tokens_per_line: Vec<Token>,
|
||||
) -> Self {
|
||||
Self {
|
||||
width_pct,
|
||||
per_line: tokens_per_line,
|
||||
scroll_offset: Some(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_up(mut self) -> Self {
|
||||
self.scroll_offset = self.scroll_offset.map(|offset| {
|
||||
if offset == 0 {
|
||||
0
|
||||
} else {
|
||||
offset - 1
|
||||
}
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
fn scroll_down(mut self) -> Self {
|
||||
// FIXME: since we don't know what the screen size is
|
||||
// when this is rendered, the scroll can become "stuck"
|
||||
// for a bit by scrolling further than the screen can allow.
|
||||
// In this case, perhaps doing a scroll-check would be useful?
|
||||
// Or, since the scroll should result in re-rendering, maybe
|
||||
// the renderer should call a check on the widgets with
|
||||
// the height at the time of the render? An extra roll
|
||||
// through the list, woo! But tbh, I should probably look at
|
||||
// optimizing later, not now.
|
||||
self.scroll_offset = self
|
||||
.scroll_offset
|
||||
.map(|offset| self.per_line.len().min(offset + 1));
|
||||
self
|
||||
}
|
||||
|
||||
fn get_line(&self, line: usize, height: usize) -> Option<&Token> {
|
||||
let len = self.per_line.len();
|
||||
if self.scroll_offset.is_none() || len < height {
|
||||
return self.per_line.get(line);
|
||||
}
|
||||
let offset = len
|
||||
- height
|
||||
- (len - height).min(self.scroll_offset.unwrap_or(0));
|
||||
self.per_line.get(offset + line)
|
||||
}
|
||||
|
||||
fn abs_width(&self, line_width: usize) -> usize {
|
||||
line_width
|
||||
.min((self.width_pct as usize * line_width) / 100 + 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,17 +218,21 @@ impl Instruction {
|
|||
return vec![];
|
||||
}
|
||||
match self {
|
||||
Instruction::FixedHeight(next, lines, wdg) => (0..lines)
|
||||
// Widget has to be taken mutably cause I can't think
|
||||
// of a simpler way to "fix" the scrolling offset
|
||||
// at the point of rendering when it's scrolled past
|
||||
// what fits on screen.
|
||||
Instruction::FixedHeight(next, lines, mut wdg) => (0
|
||||
..lines)
|
||||
.map(|line| {
|
||||
let mut offset = 0;
|
||||
(&wdg)
|
||||
(&mut wdg)
|
||||
.into_iter()
|
||||
.map(|w| {
|
||||
let (width, token) = (
|
||||
(w.want_width as usize * line_width)
|
||||
/ 100
|
||||
+ 1, // Feels wrong?
|
||||
w.get_line(line).map(|t| t.clone()),
|
||||
w.abs_width(line_width),
|
||||
w.get_line(line, lines)
|
||||
.map(|t| t.clone()),
|
||||
);
|
||||
let mut result = match token {
|
||||
Some(tok) => tok
|
||||
|
@ -248,13 +298,12 @@ impl Plan {
|
|||
}
|
||||
|
||||
fn fit_check(widgets: &Vec<Widget>) {
|
||||
if widgets
|
||||
let sum = widgets
|
||||
.into_iter()
|
||||
.map(|wd| wd.want_width as usize)
|
||||
.sum::<usize>()
|
||||
> 100
|
||||
{
|
||||
panic!("widgets do not fit screen")
|
||||
.map(|wd| wd.abs_width(100))
|
||||
.sum::<usize>();
|
||||
if sum > 100 {
|
||||
panic!("widgets do not fit screen: {}/100", sum)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -321,6 +370,49 @@ impl Plan {
|
|||
Self::Fill(Box::new(self), widgets)
|
||||
}
|
||||
|
||||
pub fn scroll_up(self) -> Self {
|
||||
match self {
|
||||
Plan::FixedHeight(next, height, widgets) => {
|
||||
Plan::FixedHeight(
|
||||
Box::new(next.scroll_up()),
|
||||
height,
|
||||
widgets
|
||||
.into_iter()
|
||||
.map(|w| w.scroll_up())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
Plan::Fill(next, widgets) => Plan::Fill(
|
||||
Box::new(next.scroll_up()),
|
||||
widgets.into_iter().map(|w| w.scroll_up()).collect(),
|
||||
),
|
||||
Plan::End => Plan::End,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_down(self) -> Self {
|
||||
match self {
|
||||
Plan::FixedHeight(next, height, widgets) => {
|
||||
Plan::FixedHeight(
|
||||
Box::new(next.scroll_down()),
|
||||
height,
|
||||
widgets
|
||||
.into_iter()
|
||||
.map(|w| w.scroll_down())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
Plan::Fill(next, widgets) => Plan::Fill(
|
||||
Box::new(next.scroll_down()),
|
||||
widgets
|
||||
.into_iter()
|
||||
.map(|w| w.scroll_down())
|
||||
.collect(),
|
||||
),
|
||||
Plan::End => Plan::End,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make(
|
||||
self,
|
||||
base_colors: ColorSet,
|
||||
|
@ -364,6 +456,18 @@ mod tests {
|
|||
bg: Color::BLACK,
|
||||
};
|
||||
let (w1, w2, w3) = test_widgets();
|
||||
let scrolling_widget = Widget::scrolling(
|
||||
100,
|
||||
(0..100)
|
||||
.into_iter()
|
||||
.map(|num| Token::text(format!("line {}", num)))
|
||||
.collect(),
|
||||
);
|
||||
let scrolled_by_3 = scrolling_widget
|
||||
.clone()
|
||||
.scroll_up()
|
||||
.scroll_up()
|
||||
.scroll_up();
|
||||
vec![
|
||||
(
|
||||
"all the widgets, 30 lines",
|
||||
|
@ -372,7 +476,7 @@ mod tests {
|
|||
vec![w1.clone(), w2.clone(), w3.clone()],
|
||||
),
|
||||
w1.clone()
|
||||
.get_line(0)
|
||||
.get_line(0, 30)
|
||||
.unwrap()
|
||||
.clone()
|
||||
.with_width(WIDTH / 3)
|
||||
|
@ -384,7 +488,7 @@ mod tests {
|
|||
])
|
||||
.chain(
|
||||
w2.clone()
|
||||
.get_line(0)
|
||||
.get_line(0, 30)
|
||||
.unwrap()
|
||||
.clone()
|
||||
.with_width(WIDTH / 3),
|
||||
|
@ -396,7 +500,7 @@ mod tests {
|
|||
])
|
||||
.chain(
|
||||
w3.clone()
|
||||
.get_line(0)
|
||||
.get_line(0, 30)
|
||||
.unwrap()
|
||||
.clone()
|
||||
.with_width(WIDTH / 3),
|
||||
|
@ -433,7 +537,7 @@ mod tests {
|
|||
"Single widget, single 10 lines section",
|
||||
Instruction::start().fixed(10, vec![w1.clone()]),
|
||||
w1.clone()
|
||||
.get_line(0)
|
||||
.get_line(0, 10)
|
||||
.unwrap()
|
||||
.clone()
|
||||
.with_width(WIDTH / 3)
|
||||
|
@ -456,6 +560,53 @@ mod tests {
|
|||
)
|
||||
.collect(),
|
||||
),
|
||||
(
|
||||
"Scrollable widget: no scrolling",
|
||||
Instruction::start()
|
||||
.fixed(10, vec![scrolling_widget]),
|
||||
(0..10)
|
||||
.into_iter()
|
||||
.map(|num| {
|
||||
vec![
|
||||
Component::String(format!(
|
||||
"line {}",
|
||||
num
|
||||
)),
|
||||
Component::Fg(BASE_COLOR.fg),
|
||||
Component::Bg(BASE_COLOR.bg),
|
||||
Component::NextLine,
|
||||
]
|
||||
})
|
||||
.flatten()
|
||||
.chain(
|
||||
(0..(HEIGHT - 10))
|
||||
.map(|_| Component::NextLine),
|
||||
)
|
||||
.collect(),
|
||||
),
|
||||
(
|
||||
"Scrollable widget: scrolled by 3",
|
||||
Instruction::start().fixed(10, vec![scrolled_by_3]),
|
||||
(0..10)
|
||||
.into_iter()
|
||||
.map(|num| {
|
||||
vec![
|
||||
Component::String(format!(
|
||||
"line {}",
|
||||
num + 3,
|
||||
)),
|
||||
Component::Fg(BASE_COLOR.fg),
|
||||
Component::Bg(BASE_COLOR.bg),
|
||||
Component::NextLine,
|
||||
]
|
||||
})
|
||||
.flatten()
|
||||
.chain(
|
||||
(0..(HEIGHT - 10))
|
||||
.map(|_| Component::NextLine),
|
||||
)
|
||||
.collect(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(|(name, instruction, expected)| {
|
||||
|
@ -466,24 +617,22 @@ mod tests {
|
|||
.into_iter()
|
||||
.zip(expected.clone())
|
||||
.enumerate()
|
||||
// .filter(|(i, (left, right))| left != right)
|
||||
.map(|(i, (act, exp))| {
|
||||
format!(
|
||||
"{}{}: {} || {}",
|
||||
"{: <3}{: <3}: [exp|act] {: <25}|{}\n",
|
||||
if act != exp { "## " } else { "" },
|
||||
i,
|
||||
exp.debug(),
|
||||
act.debug(),
|
||||
)
|
||||
.replace('\n', " ")
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
.collect::<String>();
|
||||
assert!(
|
||||
expected == actual,
|
||||
"<{}>:
|
||||
expected({})
|
||||
actual({})
|
||||
diff:\n{:#?}",
|
||||
diff:\n{}",
|
||||
name,
|
||||
expected.len(),
|
||||
actual.len(),
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
use component::{Component, LineArea, Plan, Widget};
|
||||
use std::{
|
||||
io::{Stdout, Write},
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use termion::{
|
||||
clear, cursor,
|
||||
event::{Key, MouseButton, MouseEvent},
|
||||
input::{MouseTerminal, TermRead},
|
||||
raw::{IntoRawMode, RawTerminal},
|
||||
screen::{AlternateScreen, IntoAlternateScreen},
|
||||
AsyncReader,
|
||||
};
|
||||
use theme::{Color, ColorSet};
|
||||
use token::Token;
|
||||
use view::Event;
|
||||
|
||||
type Result<T> = std::result::Result<T, anyhow::Error>;
|
||||
|
@ -23,117 +22,238 @@ pub mod theme;
|
|||
pub mod token;
|
||||
pub mod view;
|
||||
|
||||
struct PlanState {
|
||||
plans: Vec<Plan>,
|
||||
layers: Vec<Layer>,
|
||||
last_term_size: (u16, u16),
|
||||
base_colorset: ColorSet,
|
||||
last_render: Option<Instant>,
|
||||
}
|
||||
|
||||
impl PlanState {
|
||||
fn new(
|
||||
plans: PlanLayers,
|
||||
base_colorset: ColorSet,
|
||||
(term_w, term_h): (u16, u16),
|
||||
last_render: Option<Instant>,
|
||||
) -> Self {
|
||||
let layers = plans
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|plan| {
|
||||
Layer::make(
|
||||
plan,
|
||||
base_colorset,
|
||||
(term_w as usize, term_h as usize),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
plans,
|
||||
layers,
|
||||
base_colorset,
|
||||
last_render,
|
||||
last_term_size: (term_w, term_h),
|
||||
}
|
||||
}
|
||||
|
||||
fn match_click(&self, click: (u16, u16)) -> Option<String> {
|
||||
for layer in (&self.layers).into_iter().rev() {
|
||||
let (_, h) = termion::terminal_size().unwrap();
|
||||
for link in &layer.links {
|
||||
if link.1.matches(click) {
|
||||
return Some(link.0.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn from_plans(
|
||||
plans: PlanLayers,
|
||||
base_colorset: ColorSet,
|
||||
) -> Result<Self> {
|
||||
Ok(Self::new(
|
||||
plans,
|
||||
base_colorset,
|
||||
termion::terminal_size()?,
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
fn resized(self) -> Result<Self> {
|
||||
let term_size = termion::terminal_size()?;
|
||||
if term_size == self.last_term_size {
|
||||
return Ok(self);
|
||||
}
|
||||
Ok(Self::new(
|
||||
self.plans,
|
||||
self.base_colorset,
|
||||
term_size,
|
||||
self.last_render,
|
||||
))
|
||||
}
|
||||
|
||||
fn scroll_up(self) -> Result<Self> {
|
||||
Ok(Self::new(
|
||||
self.plans
|
||||
.into_iter()
|
||||
.map(|layer| layer.scroll_up())
|
||||
.collect(),
|
||||
self.base_colorset,
|
||||
termion::terminal_size()?,
|
||||
self.last_render,
|
||||
))
|
||||
}
|
||||
|
||||
fn scroll_down(self) -> Result<Self> {
|
||||
Ok(Self::new(
|
||||
self.plans
|
||||
.into_iter()
|
||||
.map(|layer| layer.scroll_down())
|
||||
.collect(),
|
||||
self.base_colorset,
|
||||
termion::terminal_size()?,
|
||||
self.last_render,
|
||||
))
|
||||
}
|
||||
|
||||
fn render(&mut self, scr: &mut Screen) -> Result<()> {
|
||||
const RENDER_COOLDOWN: u128 = 1000 / 50;
|
||||
if let Some(last) = self.last_render {
|
||||
if last.elapsed().as_millis() < RENDER_COOLDOWN {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
write!(
|
||||
scr,
|
||||
"{color}{clear}{goto}",
|
||||
color = self.base_colorset.to_string(),
|
||||
clear = clear::All,
|
||||
goto = cursor::Goto(1, 1)
|
||||
)?;
|
||||
for layer in (&self.layers).into_iter().map(|layer| {
|
||||
let mut line = 1;
|
||||
(&layer.components)
|
||||
.into_iter()
|
||||
.map(|component| match component {
|
||||
Component::NextLine => {
|
||||
line += 1;
|
||||
"\n\r".into()
|
||||
}
|
||||
Component::Link(_, _) => panic!(
|
||||
"links should be filtered out at the layer"
|
||||
),
|
||||
Component::X(x) => {
|
||||
cursor::Goto((x + 1) as u16, line).into()
|
||||
}
|
||||
Component::String(s) => s.clone(),
|
||||
Component::Fg(c) => c.fg(),
|
||||
Component::Bg(c) => c.bg(),
|
||||
})
|
||||
.collect::<String>()
|
||||
}) {
|
||||
write!(scr, "{}", layer)?;
|
||||
}
|
||||
scr.flush()?;
|
||||
self.last_render = Some(Instant::now());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run<V>(view: V) -> Result<()>
|
||||
where
|
||||
V: View,
|
||||
{
|
||||
let base_colorset = ColorSet {
|
||||
fg: Color::WHITE,
|
||||
bg: Color::BLACK,
|
||||
};
|
||||
let mut view = view;
|
||||
let mut last_ctrlc: Option<Instant> = None;
|
||||
let mut screen: Screen = MouseTerminal::from(
|
||||
std::io::stdout().into_alternate_screen()?.into_raw_mode()?,
|
||||
);
|
||||
let mut events_iter = termion::async_stdin().events();
|
||||
let mut plans = view.init()?;
|
||||
let (term_w, term_h) = termion::terminal_size()?;
|
||||
let mut layers = make_layers(
|
||||
plans,
|
||||
ColorSet {
|
||||
fg: Color::WHITE,
|
||||
bg: Color::BLACK,
|
||||
},
|
||||
(term_w as usize, term_h as usize),
|
||||
);
|
||||
render_layers(&layers, &mut screen)?;
|
||||
screen.flush()?;
|
||||
let mut plans =
|
||||
PlanState::from_plans(view.init()?, base_colorset)?;
|
||||
plans.render(&mut screen)?;
|
||||
let mut skip_resize = false;
|
||||
loop {
|
||||
let event = match events_iter.next() {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
continue;
|
||||
}
|
||||
}?;
|
||||
// if let termion::event::Event::Mouse(m) = &event {
|
||||
// match m {
|
||||
// MouseEvent::Press(btn, x, y) => {
|
||||
// eprintln!("got {:#?} at {} {}", btn, x, y);
|
||||
// }
|
||||
// _ => {}
|
||||
// }
|
||||
// } else {
|
||||
// eprintln!("got event {:#?}", &event);
|
||||
// }
|
||||
|
||||
let event = match event {
|
||||
termion::event::Event::Mouse(mus) => match mus {
|
||||
termion::event::MouseEvent::Press(_, x, y) => {
|
||||
// TODO: scrolling later
|
||||
match match_click(&layers, (x, y)) {
|
||||
Some(yay) => Event::Link(yay),
|
||||
None => continue,
|
||||
MouseEvent::Press(press, x, y) => match press {
|
||||
MouseButton::WheelUp => {
|
||||
plans = plans.scroll_up()?;
|
||||
skip_resize = true;
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => Event::Input(event),
|
||||
MouseButton::WheelDown => {
|
||||
plans = plans.scroll_down()?;
|
||||
skip_resize = true;
|
||||
None
|
||||
}
|
||||
_ => match plans.match_click((x, y)) {
|
||||
Some(name) => Some(Event::Link(name)),
|
||||
None => {
|
||||
skip_resize = true;
|
||||
None
|
||||
}
|
||||
},
|
||||
},
|
||||
// Not doing release/whatever mouse events rn
|
||||
_ => None,
|
||||
},
|
||||
_ => Event::Input(event),
|
||||
termion::event::Event::Key(key) => {
|
||||
if let Key::Ctrl(c) = key {
|
||||
if c == 'c' {
|
||||
match last_ctrlc {
|
||||
Some(last) => {
|
||||
if last.elapsed().as_millis() < 300 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
};
|
||||
last_ctrlc = Some(Instant::now());
|
||||
continue;
|
||||
}
|
||||
Some(Event::Input(event))
|
||||
} else {
|
||||
Some(Event::Input(event))
|
||||
}
|
||||
}
|
||||
_ => Some(Event::Input(event)),
|
||||
};
|
||||
|
||||
plans = view.update(event)?;
|
||||
let (term_w, term_h) = termion::terminal_size()?;
|
||||
layers = make_layers(
|
||||
plans,
|
||||
ColorSet {
|
||||
fg: Color::WHITE,
|
||||
bg: Color::BLACK,
|
||||
},
|
||||
(term_w as usize, term_h as usize),
|
||||
);
|
||||
write!(screen, "{}", clear::All)?;
|
||||
render_layers(&layers, &mut screen)?;
|
||||
screen.flush()?;
|
||||
}
|
||||
}
|
||||
|
||||
fn match_click(
|
||||
layers: &Vec<Layer>,
|
||||
click: (u16, u16),
|
||||
) -> Option<String> {
|
||||
for layer in layers.into_iter().rev() {
|
||||
for link in &layer.links {
|
||||
if link.1.matches(click) {
|
||||
return Some(link.0.clone());
|
||||
if let Some(event) = event {
|
||||
let (_, update) = view.update(event)?;
|
||||
if let Some(layers) = update {
|
||||
plans = PlanState::from_plans(layers, base_colorset)?;
|
||||
}
|
||||
} else if !skip_resize {
|
||||
plans = plans.resized()?;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn make_layers(
|
||||
plans: PlanLayers,
|
||||
base_colorset: ColorSet,
|
||||
term_dimensions: (usize, usize),
|
||||
) -> Vec<Layer> {
|
||||
plans
|
||||
.into_iter()
|
||||
.map(|plan| Layer::make(plan, base_colorset, term_dimensions))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_layers(
|
||||
layered: &Vec<Layer>,
|
||||
scr: &mut Screen,
|
||||
) -> Result<()> {
|
||||
for layer in layered.into_iter().map(|layer| {
|
||||
let mut line = 1;
|
||||
(&layer.components)
|
||||
.into_iter()
|
||||
.map(|component| match component {
|
||||
Component::NextLine => {
|
||||
line += 1;
|
||||
"\n\r".into()
|
||||
}
|
||||
Component::Link(_, _) => panic!(
|
||||
"links should be filtered out at the layer"
|
||||
),
|
||||
Component::X(x) => {
|
||||
cursor::Goto((x + 1) as u16, line).into()
|
||||
}
|
||||
Component::String(s) => s.clone(),
|
||||
Component::Fg(c) => c.fg(),
|
||||
Component::Bg(c) => c.bg(),
|
||||
})
|
||||
.collect::<String>()
|
||||
}) {
|
||||
write!(scr, "{}", layer)?;
|
||||
plans.render(&mut screen)?;
|
||||
skip_resize = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -190,11 +310,16 @@ impl Layer {
|
|||
pub type PlanLayers = Vec<Plan>;
|
||||
|
||||
pub trait View {
|
||||
type Message;
|
||||
|
||||
fn init(
|
||||
&mut self,
|
||||
) -> std::result::Result<PlanLayers, anyhow::Error>;
|
||||
fn update(
|
||||
&mut self,
|
||||
event: Event,
|
||||
) -> std::result::Result<PlanLayers, anyhow::Error>;
|
||||
) -> std::result::Result<
|
||||
(Self::Message, Option<PlanLayers>),
|
||||
anyhow::Error,
|
||||
>;
|
||||
}
|
||||
|
|
|
@ -66,10 +66,31 @@ impl Token {
|
|||
}
|
||||
}
|
||||
|
||||
// Checks if any children are Token::Centered
|
||||
// Used for links so they do not get set incorrectly
|
||||
fn has_centered(&self) -> bool {
|
||||
match self {
|
||||
Token::Centered(_) => true,
|
||||
Token::Limited(next, _) => next.has_centered(),
|
||||
Token::CharPad(next, _, _) => next.has_centered(),
|
||||
Token::Fg(next, _) => next.has_centered(),
|
||||
Token::Bg(next, _) => next.has_centered(),
|
||||
Token::Link(next, _) => next.has_centered(),
|
||||
Token::String(next, _) => next.has_centered(),
|
||||
Token::End => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn link<S>(self, name: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if self.has_centered() {
|
||||
panic!("FIXME: Token::link called after centered");
|
||||
}
|
||||
}
|
||||
Self::Link(Box::new(self), name.into())
|
||||
}
|
||||
|
||||
|
@ -252,11 +273,11 @@ mod tests {
|
|||
(
|
||||
"contains a link",
|
||||
Token::text("learn more")
|
||||
.centered()
|
||||
.link("string_link"),
|
||||
.link("string_link")
|
||||
.centered(),
|
||||
vec![
|
||||
Component::Link("string_link".into(), 10),
|
||||
Component::X((WIDTH - 10) / 2),
|
||||
Component::Link("string_link".into(), 10),
|
||||
Component::String("learn more".into()),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
use crate::{
|
||||
component::{Component, LineArea, Plan},
|
||||
theme::ColorSet,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Event {
|
||||
Link(String),
|
||||
Input(termion::event::Event),
|
||||
|
|
|
@ -33,26 +33,33 @@ impl Default for App {
|
|||
],
|
||||
)],
|
||||
),
|
||||
Plan::start().fill(vec![]).fixed(
|
||||
4,
|
||||
vec![Widget::new(
|
||||
Plan::start()
|
||||
.fixed(4, vec![])
|
||||
.fill(vec![Widget::scrolling(
|
||||
100,
|
||||
vec![
|
||||
Token::End,
|
||||
Token::text(
|
||||
"you did it! now undo it u fuck",
|
||||
)
|
||||
.bg(Color::RED)
|
||||
.link("click"),
|
||||
],
|
||||
)],
|
||||
),
|
||||
(1..=100)
|
||||
.into_iter()
|
||||
.map(|num| {
|
||||
Token::text(format!(
|
||||
"this is {}",
|
||||
num
|
||||
))
|
||||
.padded(30)
|
||||
.bg("#330033".try_into().unwrap())
|
||||
.link(format!("num{}", num))
|
||||
.centered()
|
||||
})
|
||||
.collect(),
|
||||
)])
|
||||
.fixed(4, vec![]),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl View for App {
|
||||
type Message = ();
|
||||
|
||||
fn init(
|
||||
&mut self,
|
||||
) -> std::result::Result<kkdisp::PlanLayers, anyhow::Error> {
|
||||
|
@ -62,17 +69,17 @@ impl View for App {
|
|||
fn update(
|
||||
&mut self,
|
||||
event: Event,
|
||||
) -> std::result::Result<kkdisp::PlanLayers, anyhow::Error> {
|
||||
) -> std::result::Result<
|
||||
(Self::Message, Option<kkdisp::PlanLayers>),
|
||||
anyhow::Error,
|
||||
> {
|
||||
match event {
|
||||
Event::Link(lnk) => {
|
||||
if lnk == "click" {
|
||||
self.index ^= 1;
|
||||
Ok(vec![self.states[self.index].clone()])
|
||||
} else {
|
||||
panic!("bad link");
|
||||
}
|
||||
eprintln!("recieved link: {}", lnk);
|
||||
self.index ^= 1;
|
||||
Ok(((), Some(vec![self.states[self.index].clone()])))
|
||||
}
|
||||
_ => Ok(vec![self.states[self.index].clone()]),
|
||||
_ => Ok(((), None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue