added scrolling to widgets
This commit is contained in:
parent
3a5d8e929d
commit
dafd1493fe
|
@ -248,8 +248,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termion"
|
name = "termion"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://sectorinf.com/emilis/termion#ce611b828393cf1bad1667089af9b41a7d99a768"
|
||||||
checksum = "659c1f379f3408c7e5e84c7d0da6d93404e3800b6b9d063ba24436419302ec90"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"numtoa",
|
"numtoa",
|
||||||
|
|
|
@ -7,8 +7,11 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.68"
|
anyhow = "1.0.68"
|
||||||
termion = "2.0.1"
|
# termion = "2.0.1"
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
version = "1"
|
version = "1"
|
||||||
features = ["std", "derive"]
|
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)]
|
#[derive(Debug, Clone, Eq)]
|
||||||
pub struct Widget {
|
pub struct Widget {
|
||||||
want_width: u8,
|
width_pct: u8,
|
||||||
per_line: Vec<Token>,
|
per_line: Vec<Token>,
|
||||||
|
scroll_offset: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Widget {
|
impl PartialEq for Widget {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
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.len() == other.per_line.len()
|
||||||
&& (&self.per_line)
|
&& (&self.per_line)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -111,13 +106,64 @@ impl PartialEq for Widget {
|
||||||
impl Widget {
|
impl Widget {
|
||||||
pub fn new(width_pct: u8, tokens_per_line: Vec<Token>) -> Self {
|
pub fn new(width_pct: u8, tokens_per_line: Vec<Token>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
want_width: width_pct,
|
width_pct,
|
||||||
per_line: tokens_per_line,
|
per_line: tokens_per_line,
|
||||||
|
scroll_offset: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_line(&self, line: usize) -> Option<&Token> {
|
pub fn scrolling(
|
||||||
self.per_line.get(line)
|
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![];
|
return vec![];
|
||||||
}
|
}
|
||||||
match self {
|
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| {
|
.map(|line| {
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
(&wdg)
|
(&mut wdg)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|w| {
|
.map(|w| {
|
||||||
let (width, token) = (
|
let (width, token) = (
|
||||||
(w.want_width as usize * line_width)
|
w.abs_width(line_width),
|
||||||
/ 100
|
w.get_line(line, lines)
|
||||||
+ 1, // Feels wrong?
|
.map(|t| t.clone()),
|
||||||
w.get_line(line).map(|t| t.clone()),
|
|
||||||
);
|
);
|
||||||
let mut result = match token {
|
let mut result = match token {
|
||||||
Some(tok) => tok
|
Some(tok) => tok
|
||||||
|
@ -248,13 +298,12 @@ impl Plan {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fit_check(widgets: &Vec<Widget>) {
|
fn fit_check(widgets: &Vec<Widget>) {
|
||||||
if widgets
|
let sum = widgets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|wd| wd.want_width as usize)
|
.map(|wd| wd.abs_width(100))
|
||||||
.sum::<usize>()
|
.sum::<usize>();
|
||||||
> 100
|
if sum > 100 {
|
||||||
{
|
panic!("widgets do not fit screen: {}/100", sum)
|
||||||
panic!("widgets do not fit screen")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,6 +370,49 @@ impl Plan {
|
||||||
Self::Fill(Box::new(self), widgets)
|
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(
|
pub fn make(
|
||||||
self,
|
self,
|
||||||
base_colors: ColorSet,
|
base_colors: ColorSet,
|
||||||
|
@ -364,6 +456,18 @@ mod tests {
|
||||||
bg: Color::BLACK,
|
bg: Color::BLACK,
|
||||||
};
|
};
|
||||||
let (w1, w2, w3) = test_widgets();
|
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![
|
vec![
|
||||||
(
|
(
|
||||||
"all the widgets, 30 lines",
|
"all the widgets, 30 lines",
|
||||||
|
@ -372,7 +476,7 @@ mod tests {
|
||||||
vec![w1.clone(), w2.clone(), w3.clone()],
|
vec![w1.clone(), w2.clone(), w3.clone()],
|
||||||
),
|
),
|
||||||
w1.clone()
|
w1.clone()
|
||||||
.get_line(0)
|
.get_line(0, 30)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone()
|
.clone()
|
||||||
.with_width(WIDTH / 3)
|
.with_width(WIDTH / 3)
|
||||||
|
@ -384,7 +488,7 @@ mod tests {
|
||||||
])
|
])
|
||||||
.chain(
|
.chain(
|
||||||
w2.clone()
|
w2.clone()
|
||||||
.get_line(0)
|
.get_line(0, 30)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone()
|
.clone()
|
||||||
.with_width(WIDTH / 3),
|
.with_width(WIDTH / 3),
|
||||||
|
@ -396,7 +500,7 @@ mod tests {
|
||||||
])
|
])
|
||||||
.chain(
|
.chain(
|
||||||
w3.clone()
|
w3.clone()
|
||||||
.get_line(0)
|
.get_line(0, 30)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone()
|
.clone()
|
||||||
.with_width(WIDTH / 3),
|
.with_width(WIDTH / 3),
|
||||||
|
@ -433,7 +537,7 @@ mod tests {
|
||||||
"Single widget, single 10 lines section",
|
"Single widget, single 10 lines section",
|
||||||
Instruction::start().fixed(10, vec![w1.clone()]),
|
Instruction::start().fixed(10, vec![w1.clone()]),
|
||||||
w1.clone()
|
w1.clone()
|
||||||
.get_line(0)
|
.get_line(0, 10)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone()
|
.clone()
|
||||||
.with_width(WIDTH / 3)
|
.with_width(WIDTH / 3)
|
||||||
|
@ -456,6 +560,53 @@ mod tests {
|
||||||
)
|
)
|
||||||
.collect(),
|
.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()
|
.into_iter()
|
||||||
.for_each(|(name, instruction, expected)| {
|
.for_each(|(name, instruction, expected)| {
|
||||||
|
@ -466,24 +617,22 @@ mod tests {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.zip(expected.clone())
|
.zip(expected.clone())
|
||||||
.enumerate()
|
.enumerate()
|
||||||
// .filter(|(i, (left, right))| left != right)
|
|
||||||
.map(|(i, (act, exp))| {
|
.map(|(i, (act, exp))| {
|
||||||
format!(
|
format!(
|
||||||
"{}{}: {} || {}",
|
"{: <3}{: <3}: [exp|act] {: <25}|{}\n",
|
||||||
if act != exp { "## " } else { "" },
|
if act != exp { "## " } else { "" },
|
||||||
i,
|
i,
|
||||||
exp.debug(),
|
exp.debug(),
|
||||||
act.debug(),
|
act.debug(),
|
||||||
)
|
)
|
||||||
.replace('\n', " ")
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>();
|
.collect::<String>();
|
||||||
assert!(
|
assert!(
|
||||||
expected == actual,
|
expected == actual,
|
||||||
"<{}>:
|
"<{}>:
|
||||||
expected({})
|
expected({})
|
||||||
actual({})
|
actual({})
|
||||||
diff:\n{:#?}",
|
diff:\n{}",
|
||||||
name,
|
name,
|
||||||
expected.len(),
|
expected.len(),
|
||||||
actual.len(),
|
actual.len(),
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
use component::{Component, LineArea, Plan, Widget};
|
use component::{Component, LineArea, Plan, Widget};
|
||||||
use std::{
|
use std::{
|
||||||
io::{Stdout, Write},
|
io::{Stdout, Write},
|
||||||
time::Duration,
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use termion::{
|
use termion::{
|
||||||
clear, cursor,
|
clear, cursor,
|
||||||
|
event::{Key, MouseButton, MouseEvent},
|
||||||
input::{MouseTerminal, TermRead},
|
input::{MouseTerminal, TermRead},
|
||||||
raw::{IntoRawMode, RawTerminal},
|
raw::{IntoRawMode, RawTerminal},
|
||||||
screen::{AlternateScreen, IntoAlternateScreen},
|
screen::{AlternateScreen, IntoAlternateScreen},
|
||||||
AsyncReader,
|
|
||||||
};
|
};
|
||||||
use theme::{Color, ColorSet};
|
use theme::{Color, ColorSet};
|
||||||
use token::Token;
|
|
||||||
use view::Event;
|
use view::Event;
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, anyhow::Error>;
|
type Result<T> = std::result::Result<T, anyhow::Error>;
|
||||||
|
@ -23,117 +22,238 @@ pub mod theme;
|
||||||
pub mod token;
|
pub mod token;
|
||||||
pub mod view;
|
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<()>
|
pub async fn run<V>(view: V) -> Result<()>
|
||||||
where
|
where
|
||||||
V: View,
|
V: View,
|
||||||
{
|
{
|
||||||
|
let base_colorset = ColorSet {
|
||||||
|
fg: Color::WHITE,
|
||||||
|
bg: Color::BLACK,
|
||||||
|
};
|
||||||
let mut view = view;
|
let mut view = view;
|
||||||
|
let mut last_ctrlc: Option<Instant> = None;
|
||||||
let mut screen: Screen = MouseTerminal::from(
|
let mut screen: Screen = MouseTerminal::from(
|
||||||
std::io::stdout().into_alternate_screen()?.into_raw_mode()?,
|
std::io::stdout().into_alternate_screen()?.into_raw_mode()?,
|
||||||
);
|
);
|
||||||
let mut events_iter = termion::async_stdin().events();
|
let mut events_iter = termion::async_stdin().events();
|
||||||
let mut plans = view.init()?;
|
let mut plans =
|
||||||
let (term_w, term_h) = termion::terminal_size()?;
|
PlanState::from_plans(view.init()?, base_colorset)?;
|
||||||
let mut layers = make_layers(
|
plans.render(&mut screen)?;
|
||||||
plans,
|
let mut skip_resize = false;
|
||||||
ColorSet {
|
|
||||||
fg: Color::WHITE,
|
|
||||||
bg: Color::BLACK,
|
|
||||||
},
|
|
||||||
(term_w as usize, term_h as usize),
|
|
||||||
);
|
|
||||||
render_layers(&layers, &mut screen)?;
|
|
||||||
screen.flush()?;
|
|
||||||
loop {
|
loop {
|
||||||
let event = match events_iter.next() {
|
let event = match events_iter.next() {
|
||||||
Some(e) => e,
|
Some(e) => e,
|
||||||
None => {
|
None => {
|
||||||
std::thread::sleep(Duration::from_millis(50));
|
std::thread::sleep(Duration::from_millis(1));
|
||||||
continue;
|
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 {
|
let event = match event {
|
||||||
termion::event::Event::Mouse(mus) => match mus {
|
termion::event::Event::Mouse(mus) => match mus {
|
||||||
termion::event::MouseEvent::Press(_, x, y) => {
|
MouseEvent::Press(press, x, y) => match press {
|
||||||
// TODO: scrolling later
|
MouseButton::WheelUp => {
|
||||||
match match_click(&layers, (x, y)) {
|
plans = plans.scroll_up()?;
|
||||||
Some(yay) => Event::Link(yay),
|
skip_resize = true;
|
||||||
None => continue,
|
None
|
||||||
}
|
}
|
||||||
}
|
MouseButton::WheelDown => {
|
||||||
_ => Event::Input(event),
|
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)?;
|
if let Some(event) = event {
|
||||||
let (term_w, term_h) = termion::terminal_size()?;
|
let (_, update) = view.update(event)?;
|
||||||
layers = make_layers(
|
if let Some(layers) = update {
|
||||||
plans,
|
plans = PlanState::from_plans(layers, base_colorset)?;
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
} else if !skip_resize {
|
||||||
|
plans = plans.resized()?;
|
||||||
}
|
}
|
||||||
}
|
plans.render(&mut screen)?;
|
||||||
None
|
skip_resize = false;
|
||||||
}
|
|
||||||
|
|
||||||
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)?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -190,11 +310,16 @@ impl Layer {
|
||||||
pub type PlanLayers = Vec<Plan>;
|
pub type PlanLayers = Vec<Plan>;
|
||||||
|
|
||||||
pub trait View {
|
pub trait View {
|
||||||
|
type Message;
|
||||||
|
|
||||||
fn init(
|
fn init(
|
||||||
&mut self,
|
&mut self,
|
||||||
) -> std::result::Result<PlanLayers, anyhow::Error>;
|
) -> std::result::Result<PlanLayers, anyhow::Error>;
|
||||||
fn update(
|
fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: Event,
|
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
|
pub fn link<S>(self, name: S) -> Self
|
||||||
where
|
where
|
||||||
S: Into<String>,
|
S: Into<String>,
|
||||||
{
|
{
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
if self.has_centered() {
|
||||||
|
panic!("FIXME: Token::link called after centered");
|
||||||
|
}
|
||||||
|
}
|
||||||
Self::Link(Box::new(self), name.into())
|
Self::Link(Box::new(self), name.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,11 +273,11 @@ mod tests {
|
||||||
(
|
(
|
||||||
"contains a link",
|
"contains a link",
|
||||||
Token::text("learn more")
|
Token::text("learn more")
|
||||||
.centered()
|
.link("string_link")
|
||||||
.link("string_link"),
|
.centered(),
|
||||||
vec![
|
vec![
|
||||||
Component::Link("string_link".into(), 10),
|
|
||||||
Component::X((WIDTH - 10) / 2),
|
Component::X((WIDTH - 10) / 2),
|
||||||
|
Component::Link("string_link".into(), 10),
|
||||||
Component::String("learn more".into()),
|
Component::String("learn more".into()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
use crate::{
|
#[derive(Clone, Debug)]
|
||||||
component::{Component, LineArea, Plan},
|
|
||||||
theme::ColorSet,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Link(String),
|
Link(String),
|
||||||
Input(termion::event::Event),
|
Input(termion::event::Event),
|
||||||
|
|
|
@ -33,26 +33,33 @@ impl Default for App {
|
||||||
],
|
],
|
||||||
)],
|
)],
|
||||||
),
|
),
|
||||||
Plan::start().fill(vec![]).fixed(
|
Plan::start()
|
||||||
4,
|
.fixed(4, vec![])
|
||||||
vec![Widget::new(
|
.fill(vec![Widget::scrolling(
|
||||||
100,
|
100,
|
||||||
vec![
|
(1..=100)
|
||||||
Token::End,
|
.into_iter()
|
||||||
Token::text(
|
.map(|num| {
|
||||||
"you did it! now undo it u fuck",
|
Token::text(format!(
|
||||||
)
|
"this is {}",
|
||||||
.bg(Color::RED)
|
num
|
||||||
.link("click"),
|
))
|
||||||
],
|
.padded(30)
|
||||||
)],
|
.bg("#330033".try_into().unwrap())
|
||||||
),
|
.link(format!("num{}", num))
|
||||||
|
.centered()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)])
|
||||||
|
.fixed(4, vec![]),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl View for App {
|
impl View for App {
|
||||||
|
type Message = ();
|
||||||
|
|
||||||
fn init(
|
fn init(
|
||||||
&mut self,
|
&mut self,
|
||||||
) -> std::result::Result<kkdisp::PlanLayers, anyhow::Error> {
|
) -> std::result::Result<kkdisp::PlanLayers, anyhow::Error> {
|
||||||
|
@ -62,17 +69,17 @@ impl View for App {
|
||||||
fn update(
|
fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: Event,
|
event: Event,
|
||||||
) -> std::result::Result<kkdisp::PlanLayers, anyhow::Error> {
|
) -> std::result::Result<
|
||||||
|
(Self::Message, Option<kkdisp::PlanLayers>),
|
||||||
|
anyhow::Error,
|
||||||
|
> {
|
||||||
match event {
|
match event {
|
||||||
Event::Link(lnk) => {
|
Event::Link(lnk) => {
|
||||||
if lnk == "click" {
|
eprintln!("recieved link: {}", lnk);
|
||||||
self.index ^= 1;
|
self.index ^= 1;
|
||||||
Ok(vec![self.states[self.index].clone()])
|
Ok(((), Some(vec![self.states[self.index].clone()])))
|
||||||
} else {
|
|
||||||
panic!("bad link");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => Ok(vec![self.states[self.index].clone()]),
|
_ => Ok(((), None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue