diff --git a/Cargo.toml b/Cargo.toml
index bbcd34f..b9d11fd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,3 +22,4 @@ anyhow = "1.0.89"
surf = { version = "2.3.2", default-features = false, features = ["h1-client"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
+mexprp = { version = "0.3.1", default-features = false }
diff --git a/README.md b/README.md
index fa265a0..1e558c4 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,7 @@ forms, and more!
+
## Shoutouts
diff --git a/examples/README.md b/examples/README.md
index 1118d52..55dec85 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -9,6 +9,7 @@ To run any of the examples, use `cargo run --example NAME`. For example, to run
|Example|Preview|
|---|:---:|
|[borders.rs](./borders.rs)
Showcases various border styles.||
+|[calculator.rs](./calculator.rs)
Uses clickable buttons to provide a calculator app with light/dark mode themes.||
|[context.rs](./context.rs)
Demonstrates using a custom context via `ContextProvider` and `use_context`.||
|[counter.rs](./counter.rs)
Renders a dynamic component which spawns a future to increment a counter every 100ms.||
|[form.rs](./form.rs)
Displays a form prompting the user for input into multiple text fields. Uses mutable reference props to surface the user's input to the caller once the form is submitted.||
diff --git a/examples/calculator.rs b/examples/calculator.rs
new file mode 100644
index 0000000..6dca576
--- /dev/null
+++ b/examples/calculator.rs
@@ -0,0 +1,408 @@
+use iocraft::prelude::*;
+
+#[derive(Clone, Copy, Default)]
+enum Theme {
+ #[default]
+ Dark,
+ Light,
+}
+
+#[derive(Clone, Copy)]
+struct ButtonStyle {
+ color: Color,
+ text_color: Color,
+ trim_color: Color,
+}
+
+// https://www.ditig.com/publications/256-colors-cheat-sheet
+impl Theme {
+ fn toggled(&self) -> Self {
+ match self {
+ Self::Dark => Self::Light,
+ Self::Light => Self::Dark,
+ }
+ }
+
+ fn background_color(&self) -> Color {
+ match self {
+ Self::Dark => Color::AnsiValue(237),
+ Self::Light => Color::AnsiValue(253),
+ }
+ }
+
+ fn footer_background_color(&self) -> Color {
+ match self {
+ Self::Dark => Color::AnsiValue(253),
+ Self::Light => Color::AnsiValue(237),
+ }
+ }
+
+ fn footer_text_color(&self) -> Color {
+ match self {
+ Self::Dark => Color::AnsiValue(237),
+ Self::Light => Color::AnsiValue(253),
+ }
+ }
+
+ fn screen_color(&self) -> Color {
+ Color::AnsiValue(68)
+ }
+
+ fn screen_text_color(&self) -> Color {
+ Color::AnsiValue(231)
+ }
+
+ fn screen_trim_color(&self) -> Color {
+ Color::AnsiValue(75)
+ }
+
+ fn numpad_button_style(&self) -> ButtonStyle {
+ match self {
+ Self::Dark => ButtonStyle {
+ color: Color::AnsiValue(239),
+ text_color: Color::AnsiValue(231),
+ trim_color: Color::AnsiValue(243),
+ },
+ Self::Light => ButtonStyle {
+ color: Color::AnsiValue(251),
+ text_color: Color::AnsiValue(16),
+ trim_color: Color::AnsiValue(255),
+ },
+ }
+ }
+
+ fn operator_button_style(&self) -> ButtonStyle {
+ ButtonStyle {
+ color: Color::AnsiValue(172),
+ text_color: Color::AnsiValue(231),
+ trim_color: Color::AnsiValue(215),
+ }
+ }
+
+ fn clear_button_style(&self) -> ButtonStyle {
+ ButtonStyle {
+ color: Color::AnsiValue(161),
+ text_color: Color::AnsiValue(231),
+ trim_color: Color::AnsiValue(205),
+ }
+ }
+
+ fn fn_button_style(&self) -> ButtonStyle {
+ ButtonStyle {
+ color: Color::AnsiValue(66),
+ text_color: Color::AnsiValue(231),
+ trim_color: Color::AnsiValue(115),
+ }
+ }
+}
+
+#[derive(Default, Props)]
+struct ScreenProps {
+ content: String,
+}
+
+#[component]
+fn Screen(hooks: Hooks, props: &ScreenProps) -> impl Into> {
+ let theme = hooks.use_context::();
+ element! {
+ Box(
+ width: 100pct,
+ border_style: BorderStyle::Custom(BorderCharacters {
+ top: '▁',
+ ..Default::default()
+ }),
+ border_edges: Edges::Top,
+ border_color: theme.screen_trim_color(),
+ ) {
+ Box(
+ width: 100pct,
+ background_color: theme.screen_color(),
+ padding: 1,
+ justify_content: JustifyContent::End,
+ ) {
+ Text(
+ content: &props.content,
+ align: TextAlign::Right,
+ color: theme.screen_text_color(),
+ )
+ }
+ }
+ }
+}
+
+#[derive(Default, Props)]
+struct ButtonProps {
+ label: String,
+ style: Option,
+ on_click: Handler<'static, ()>,
+}
+
+#[component]
+fn Button(props: &mut ButtonProps, mut hooks: Hooks) -> impl Into> {
+ let style = props.style.unwrap();
+
+ hooks.use_local_terminal_events({
+ let mut on_click = std::mem::take(&mut props.on_click);
+ move |event| match event {
+ TerminalEvent::FullscreenMouse(FullscreenMouseEvent { kind, .. })
+ if matches!(kind, MouseEventKind::Down(_)) =>
+ {
+ on_click(());
+ }
+ _ => {}
+ }
+ });
+
+ element! {
+ Box(
+ border_style: BorderStyle::Custom(BorderCharacters {
+ top: '▁',
+ ..Default::default()
+ }),
+ border_edges: Edges::Top,
+ border_color: style.trim_color,
+ flex_grow: 1.0,
+ margin_left: 1,
+ margin_right: 1,
+ ) {
+ Box(
+ background_color: style.color,
+ justify_content: JustifyContent::Center,
+ align_items: AlignItems::Center,
+ height: 3,
+ flex_grow: 1.0,
+ ) {
+ Text(
+ content: &props.label,
+ color: style.text_color,
+ weight: Weight::Bold,
+ )
+ }
+ }
+ }
+}
+
+#[component]
+fn Calculator(mut hooks: Hooks) -> impl Into> {
+ let theme = hooks.use_context::();
+ let numpad_button_style = theme.numpad_button_style();
+ let operator_button_style = theme.operator_button_style();
+ let fn_button_style = theme.fn_button_style();
+ let expr = hooks.use_state(|| "0".to_string());
+ let clear_on_number = hooks.use_state(|| true);
+
+ let handle_backspace = move || {
+ let new_expr = expr
+ .read()
+ .chars()
+ .take(expr.read().len() - 1)
+ .collect::();
+ if new_expr.is_empty() {
+ expr.set("0".to_string());
+ clear_on_number.set(true);
+ } else {
+ expr.set(new_expr);
+ clear_on_number.set(false);
+ }
+ };
+
+ let handle_number = move |n: u8| {
+ if clear_on_number.get() {
+ expr.set(n.to_string());
+ clear_on_number.set(false);
+ } else {
+ expr.set(expr.to_string() + &n.to_string());
+ }
+ };
+
+ let handle_decimal = move || {
+ if clear_on_number.get() {
+ expr.set("0.".to_string());
+ clear_on_number.set(false);
+ } else if expr.read().chars().last() != Some('.') {
+ expr.set(expr.to_string() + ".");
+ }
+ };
+
+ let handle_clear = move || {
+ expr.set("0".to_string());
+ clear_on_number.set(true);
+ };
+
+ let has_trailing_operator = matches!(
+ expr.read().chars().last(),
+ Some('+') | Some('-') | Some('×') | Some('÷')
+ );
+
+ let handle_operator = move |op: char| {
+ if clear_on_number.get() {
+ clear_on_number.set(false);
+ }
+ if !has_trailing_operator {
+ expr.set(expr.to_string() + &op.to_string());
+ }
+ };
+
+ let handle_percent = move || {
+ if clear_on_number.get() {
+ clear_on_number.set(false);
+ }
+ if !has_trailing_operator {
+ expr.set(expr.to_string() + "%");
+ }
+ };
+
+ let handle_plus_minus = move || {
+ if clear_on_number.get() {
+ clear_on_number.set(false);
+ }
+ if !has_trailing_operator {
+ expr.set(format!("-({})", expr));
+ }
+ };
+
+ let handle_equals = move || {
+ if let Ok(f) = mexprp::eval::(&expr.to_string()) {
+ expr.set(f.to_string());
+ clear_on_number.set(true);
+ }
+ };
+
+ hooks.use_terminal_events({
+ move |event| match event {
+ TerminalEvent::Key(KeyEvent { code, kind, .. }) if kind != KeyEventKind::Release => {
+ match code {
+ KeyCode::Char('/') => handle_operator('÷'),
+ KeyCode::Char('*') => handle_operator('×'),
+ KeyCode::Char('+') => handle_operator('+'),
+ KeyCode::Char('-') => handle_operator('-'),
+ KeyCode::Char('0') => handle_number(0),
+ KeyCode::Char('1') => handle_number(1),
+ KeyCode::Char('2') => handle_number(2),
+ KeyCode::Char('3') => handle_number(3),
+ KeyCode::Char('4') => handle_number(4),
+ KeyCode::Char('5') => handle_number(5),
+ KeyCode::Char('6') => handle_number(6),
+ KeyCode::Char('7') => handle_number(7),
+ KeyCode::Char('8') => handle_number(8),
+ KeyCode::Char('9') => handle_number(9),
+ KeyCode::Char('.') => handle_decimal(),
+ KeyCode::Char('%') => handle_percent(),
+ KeyCode::Char('=') | KeyCode::Enter => handle_equals(),
+ KeyCode::Backspace => handle_backspace(),
+ KeyCode::Char('c') => handle_clear(),
+ _ => {}
+ }
+ }
+ _ => {}
+ }
+ });
+
+ element! {
+ Box(
+ width: 100pct,
+ height: 100pct,
+ flex_direction: FlexDirection::Column,
+ padding_left: 1,
+ padding_right: 1,
+ ) {
+ Box(
+ padding_left: 1,
+ padding_right: 1,
+ ) {
+ Screen(content: expr.to_string())
+ }
+ Box(width: 100pct) {
+ Button(label: "←", style: fn_button_style, on_click: move |_| handle_backspace())
+ Button(label: "±", style: fn_button_style, on_click: move |_| handle_plus_minus())
+ Button(label: "%", style: fn_button_style, on_click: move |_| handle_percent())
+ Button(label: "÷", style: operator_button_style, on_click: move |_| handle_operator('÷'))
+ }
+ Box(width: 100pct) {
+ Button(label: "7", style: numpad_button_style, on_click: move |_| handle_number(7))
+ Button(label: "8", style: numpad_button_style, on_click: move |_| handle_number(8))
+ Button(label: "9", style: numpad_button_style, on_click: move |_| handle_number(9))
+ Button(label: "×", style: operator_button_style, on_click: move |_| handle_operator('×'))
+ }
+ Box(width: 100pct) {
+ Button(label: "4", style: numpad_button_style, on_click: move |_| handle_number(4))
+ Button(label: "5", style: numpad_button_style, on_click: move |_| handle_number(5))
+ Button(label: "6", style: numpad_button_style, on_click: move |_| handle_number(6))
+ Button(label: "-", style: operator_button_style, on_click: move |_| handle_operator('-'))
+ }
+ Box(width: 100pct) {
+ Button(label: "1", style: numpad_button_style, on_click: move |_| handle_number(1))
+ Button(label: "2", style: numpad_button_style, on_click: move |_| handle_number(2))
+ Button(label: "3", style: numpad_button_style, on_click: move |_| handle_number(3))
+ Button(label: "+", style: operator_button_style, on_click: move |_| handle_operator('+'))
+ }
+ Box(width: 100pct) {
+ Button(label: "C", style: theme.clear_button_style(), on_click: move |_| handle_clear())
+ Button(label: "0", style: numpad_button_style, on_click: move |_| handle_number(0))
+ Button(label: ".", style: numpad_button_style, on_click: move |_| handle_decimal())
+ Button(label: "=", style: operator_button_style, on_click: move |_| handle_equals())
+ }
+ }
+ }
+}
+
+#[component]
+fn App(mut hooks: Hooks) -> impl Into> {
+ let (width, height) = hooks.use_terminal_size();
+ let mut system = hooks.use_context_mut::();
+ let should_exit = hooks.use_state(|| false);
+ let theme = hooks.use_state(|| Theme::default());
+
+ hooks.use_terminal_events({
+ move |event| match event {
+ TerminalEvent::Key(KeyEvent { code, kind, .. }) if kind != KeyEventKind::Release => {
+ match code {
+ KeyCode::Char('q') => should_exit.set(true),
+ KeyCode::Char('t') => theme.set(theme.get().toggled()),
+ _ => {}
+ }
+ }
+ _ => {}
+ }
+ });
+
+ if should_exit.get() {
+ system.exit();
+ }
+
+ element! {
+ Box(
+ // subtract one in case there's a scrollbar
+ width: width - 1,
+ height: height,
+ background_color: theme.get().background_color(),
+ flex_direction: FlexDirection::Column,
+ gap: 1,
+ ) {
+ Box(
+ flex_grow: 1.0,
+ ) {
+ Box(
+ max_width: 120,
+ max_height: 40,
+ flex_grow: 1.0,
+ ) {
+ ContextProvider(value: Context::owned(theme.get())) {
+ Calculator
+ }
+ }
+ }
+ Box(
+ height: 1,
+ background_color: theme.get().footer_background_color(),
+ padding_left: 1,
+ ) {
+ Text(content: "[T] Toggle Theme [Q] Quit", color: theme.get().footer_text_color())
+ }
+ }
+ }
+}
+
+fn main() {
+ smol::block_on(element!(App).fullscreen()).unwrap();
+}
diff --git a/examples/images/calculator.png b/examples/images/calculator.png
new file mode 100644
index 0000000..2b809f0
Binary files /dev/null and b/examples/images/calculator.png differ
diff --git a/packages/iocraft/src/component.rs b/packages/iocraft/src/component.rs
index b085a84..08a3934 100644
--- a/packages/iocraft/src/component.rs
+++ b/packages/iocraft/src/component.rs
@@ -186,10 +186,36 @@ impl InstantiatedComponent {
}
pub fn draw(&mut self, drawer: &mut ComponentDrawer<'_>) {
- self.hooks.pre_component_draw(drawer);
- self.component.draw(drawer);
+ if self.has_transparent_layout {
+ // If the component has a transparent layout, provide the first child's layout to the
+ // hooks and component.
+ if let Some((_, child)) = self.children.components.iter().next().as_ref() {
+ drawer.for_child_node_layout(child.node_id, |drawer| {
+ self.hooks.pre_component_draw(drawer);
+ self.component.draw(drawer);
+ });
+ } else {
+ self.hooks.pre_component_draw(drawer);
+ self.component.draw(drawer);
+ }
+ } else {
+ self.hooks.pre_component_draw(drawer);
+ self.component.draw(drawer);
+ }
+
self.children.draw(drawer);
- self.hooks.post_component_draw(drawer);
+
+ if self.has_transparent_layout {
+ if let Some((_, child)) = self.children.components.iter().next().as_ref() {
+ drawer.for_child_node_layout(child.node_id, |drawer| {
+ self.hooks.post_component_draw(drawer);
+ });
+ } else {
+ self.hooks.post_component_draw(drawer);
+ }
+ } else {
+ self.hooks.post_component_draw(drawer);
+ }
}
pub async fn wait(&mut self) {
diff --git a/packages/iocraft/src/handler.rs b/packages/iocraft/src/handler.rs
index ab6aac3..86bc895 100644
--- a/packages/iocraft/src/handler.rs
+++ b/packages/iocraft/src/handler.rs
@@ -4,11 +4,18 @@ use std::ops::{Deref, DerefMut};
///
/// Any function that takes a single argument and returns `()` can be converted into a `Handler`,
/// and it can be invoked using function call syntax.
-pub struct Handler<'a, T>(Box);
+pub struct Handler<'a, T>(bool, Box);
+
+impl<'a, T> Handler<'a, T> {
+ /// Returns `true` if the handler was default-initialized.
+ pub fn is_default(&self) -> bool {
+ !self.0
+ }
+}
impl<'a, T> Default for Handler<'a, T> {
fn default() -> Self {
- Self::from(|_| {})
+ Self(false, Box::new(|_| {}))
}
}
@@ -17,7 +24,7 @@ where
F: FnMut(T) + Send + 'a,
{
fn from(f: F) -> Self {
- Self(Box::new(f))
+ Self(true, Box::new(f))
}
}
@@ -25,13 +32,13 @@ impl<'a, T: 'a> Deref for Handler<'a, T> {
type Target = dyn FnMut(T) + Send + 'a;
fn deref(&self) -> &Self::Target {
- &self.0
+ &self.1
}
}
impl<'a, T: 'a> DerefMut for Handler<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.0
+ &mut self.1
}
}
@@ -44,11 +51,13 @@ mod tests {
let mut handler = Handler::::default();
handler(0);
handler(0);
+ assert!(handler.is_default());
let mut handler = Handler::from(|value| {
assert_eq!(value, 42);
});
handler(42);
handler(42);
+ assert!(!handler.is_default());
}
}
diff --git a/packages/iocraft/src/hooks/use_terminal_events.rs b/packages/iocraft/src/hooks/use_terminal_events.rs
index 8ef1305..81506a1 100644
--- a/packages/iocraft/src/hooks/use_terminal_events.rs
+++ b/packages/iocraft/src/hooks/use_terminal_events.rs
@@ -1,9 +1,10 @@
-use crate::{ComponentUpdater, Hook, Hooks, TerminalEvent, TerminalEvents};
+use crate::{ComponentUpdater, FullscreenMouseEvent, Hook, Hooks, TerminalEvent, TerminalEvents};
use futures::stream::Stream;
use std::{
pin::{pin, Pin},
task::{Context, Poll},
};
+use taffy::{Point, Size};
/// `UseTerminalEvents` is a hook that allows you to listen for user input such as key strokes.
///
@@ -84,9 +85,22 @@ use std::{
/// ```
pub trait UseTerminalEvents {
/// Defines a callback to be invoked whenever a terminal event occurs.
+ ///
+ /// This hook will be called for all terminal events, including those that occur outside of the
+ /// component. If you only want to listen for events within the component, use
+ /// [`Self::use_local_terminal_events`] instead.
fn use_terminal_events(&mut self, f: F)
where
F: FnMut(TerminalEvent) + Send + 'static;
+
+ /// Defines a callback to be invoked whenever a terminal event occurs within a component.
+ ///
+ /// Unlike [`Self::use_terminal_events`], this hook will not be called for events such as mouse
+ /// events that occur outside of the component. Furthermore, coordinates will be translated to
+ /// component-local coordinates.
+ fn use_local_terminal_events(&mut self, f: F)
+ where
+ F: FnMut(TerminalEvent) + Send + 'static;
}
impl UseTerminalEvents for Hooks<'_, '_> {
@@ -96,6 +110,20 @@ impl UseTerminalEvents for Hooks<'_, '_> {
{
self.use_hook(move || UseTerminalEventsImpl {
events: None,
+ component_location: Default::default(),
+ in_component: false,
+ f: Box::new(f),
+ });
+ }
+
+ fn use_local_terminal_events(&mut self, f: F)
+ where
+ F: FnMut(TerminalEvent) + Send + 'static,
+ {
+ self.use_hook(move || UseTerminalEventsImpl {
+ events: None,
+ component_location: Default::default(),
+ in_component: true,
f: Box::new(f),
});
}
@@ -103,6 +131,8 @@ impl UseTerminalEvents for Hooks<'_, '_> {
struct UseTerminalEventsImpl {
events: Option,
+ component_location: (Point, Size),
+ in_component: bool,
f: Box,
}
@@ -113,7 +143,29 @@ impl Hook for UseTerminalEventsImpl {
.as_mut()
.map(|events| pin!(events).poll_next(cx))
{
- (self.f)(event);
+ if self.in_component {
+ let (location, size) = self.component_location;
+ match event {
+ TerminalEvent::FullscreenMouse(event) => {
+ if event.row >= location.y && event.column >= location.x {
+ let row = event.row - location.y;
+ let column = event.column - location.x;
+ if row < size.height && column < size.width {
+ (self.f)(TerminalEvent::FullscreenMouse(FullscreenMouseEvent {
+ row,
+ column,
+ ..event
+ }));
+ }
+ }
+ }
+ TerminalEvent::Key(_) | TerminalEvent::Resize(..) => {
+ (self.f)(event);
+ }
+ }
+ } else {
+ (self.f)(event);
+ }
}
Poll::Pending
}
@@ -123,11 +175,16 @@ impl Hook for UseTerminalEventsImpl {
self.events = updater.terminal_events();
}
}
+
+ fn post_component_draw(&mut self, drawer: &mut crate::ComponentDrawer) {
+ self.component_location = (drawer.canvas_position(), drawer.size());
+ }
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
+ use crossterm::event::MouseButton;
use futures::stream::{self, StreamExt};
use macro_rules_attribute::apply;
use smol_macros::test;
@@ -164,4 +221,54 @@ mod tests {
let expected = vec!["", "received event\n"];
assert_eq!(actual, expected);
}
+
+ #[component]
+ fn MyClickableComponent(mut hooks: Hooks) -> impl Into> {
+ let mut system = hooks.use_context_mut::();
+ let should_exit = hooks.use_state(|| false);
+ hooks.use_local_terminal_events(move |event| {
+ if let TerminalEvent::FullscreenMouse(FullscreenMouseEvent {
+ kind: MouseEventKind::Down(MouseButton::Left),
+ row,
+ column,
+ ..
+ }) = event
+ {
+ assert_eq!(row, 8);
+ assert_eq!(column, 8);
+ should_exit.set(true);
+ }
+ });
+
+ if should_exit.get() {
+ system.exit();
+ element!(Text(content:"received click")).into_any()
+ } else {
+ element!(Box(width: 10, height: 10)).into_any()
+ }
+ }
+
+ #[apply(test!)]
+ async fn test_use_local_terminal_events() {
+ let canvases: Vec<_> = element! {
+ Box(padding: 2) {
+ MyClickableComponent
+ }
+ }
+ .mock_terminal_render_loop(MockTerminalConfig::with_events(stream::iter(vec![
+ TerminalEvent::FullscreenMouse(FullscreenMouseEvent {
+ column: 10,
+ row: 10,
+ modifiers: KeyModifiers::empty(),
+ kind: MouseEventKind::Down(MouseButton::Left),
+ }),
+ ])))
+ .collect()
+ .await;
+ let actual = canvases
+ .iter()
+ .map(|c| c.to_string().trim().to_string())
+ .collect::>();
+ assert_eq!(actual, vec!["", "received click"]);
+ }
}
diff --git a/packages/iocraft/src/terminal.rs b/packages/iocraft/src/terminal.rs
index 0ce3da5..b02bee5 100644
--- a/packages/iocraft/src/terminal.rs
+++ b/packages/iocraft/src/terminal.rs
@@ -19,7 +19,7 @@ use std::{
};
// Re-exports for basic types.
-pub use crossterm::event::{KeyCode, KeyEventKind, KeyEventState, KeyModifiers};
+pub use crossterm::event::{KeyCode, KeyEventKind, KeyEventState, KeyModifiers, MouseEventKind};
/// An event fired when a key is pressed.
#[derive(Clone, Debug)]
@@ -34,12 +34,31 @@ pub struct KeyEvent {
pub kind: KeyEventKind,
}
+/// An event fired when the mouse is moved, clicked, scrolled, etc. in fullscreen mode.
+#[non_exhaustive]
+#[derive(Clone, Debug)]
+pub struct FullscreenMouseEvent {
+ /// The modifiers that were active when the event occurred.
+ pub modifiers: KeyModifiers,
+
+ /// The column that the event occurred on.
+ pub column: u16,
+
+ /// The row that the event occurred on.
+ pub row: u16,
+
+ /// The kind of mouse event.
+ pub kind: MouseEventKind,
+}
+
/// An event fired by the terminal.
#[non_exhaustive]
#[derive(Clone, Debug)]
pub enum TerminalEvent {
/// A key event, fired when a key is pressed.
Key(KeyEvent),
+ /// A mouse event, fired when the mouse is moved, clicked, scrolled, etc. in fullscreen mode.
+ FullscreenMouse(FullscreenMouseEvent),
/// A resize event, fired when the terminal is resized.
Resize(u16, u16),
}
@@ -80,6 +99,7 @@ struct StdTerminal {
dest: io::Stdout,
fullscreen: bool,
raw_mode_enabled: bool,
+ enabled_keyboard_enhancement: bool,
prev_canvas_height: u16,
}
@@ -135,6 +155,14 @@ impl TerminalImpl for StdTerminal {
modifiers: event.modifiers,
kind: event.kind,
})),
+ Ok(Event::Mouse(event)) => {
+ Some(TerminalEvent::FullscreenMouse(FullscreenMouseEvent {
+ modifiers: event.modifiers,
+ column: event.column,
+ row: event.row,
+ kind: event.kind,
+ }))
+ }
Ok(Event::Resize(width, height)) => Some(TerminalEvent::Resize(width, height)),
_ => None,
}
@@ -157,6 +185,7 @@ impl StdTerminal {
dest,
fullscreen,
raw_mode_enabled: false,
+ enabled_keyboard_enhancement: false,
prev_canvas_height: 0,
})
}
@@ -171,6 +200,7 @@ impl StdTerminal {
event::KeyboardEnhancementFlags::REPORT_EVENT_TYPES
)
)?;
+ self.enabled_keyboard_enhancement = true;
}
if self.fullscreen {
execute!(self.dest, event::EnableMouseCapture)?;
@@ -181,7 +211,7 @@ impl StdTerminal {
if self.fullscreen {
execute!(self.dest, event::DisableMouseCapture)?;
}
- if terminal::supports_keyboard_enhancement()? {
+ if self.enabled_keyboard_enhancement {
execute!(self.dest, event::PopKeyboardEnhancementFlags)?;
}
}