From c286dacb54d5134015eb77bc0df459c7504c8b69 Mon Sep 17 00:00:00 2001 From: Bastien Dejean Date: Sun, 6 Dec 2020 10:31:33 +0100 Subject: [PATCH] Add two screen regions --- artworks/touch_regions.svg | 2 +- doc/MANUAL.md | 16 ++--- src/app.rs | 26 ++++---- src/emulator.rs | 7 ++ src/geom.rs | 105 ++++++++++++++++++++++++++++++ src/gesture.rs | 24 +++++++ src/settings/mod.rs | 4 ++ src/view/common.rs | 6 +- src/view/mod.rs | 2 + src/view/reader/mod.rs | 103 +++++++++++++---------------- src/view/touch_events/mod.rs | 123 +++++++++++++++++++++++++++++++++++ 11 files changed, 339 insertions(+), 79 deletions(-) create mode 100644 src/view/touch_events/mod.rs diff --git a/artworks/touch_regions.svg b/artworks/touch_regions.svg index db0ff26..716200c 100644 --- a/artworks/touch_regions.svg +++ b/artworks/touch_regions.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/doc/MANUAL.md b/doc/MANUAL.md index f742c77..b3b8fe0 100644 --- a/doc/MANUAL.md +++ b/doc/MANUAL.md @@ -68,23 +68,23 @@ Tap the library label to bring up the library menu. ## Viewer -The screen is divided into seven regions: +The screen is divided into nine regions: ![Touch Regions](../artworks/touch_regions.svg) Tap gestures by region: -- *LE* (Left Ear): +- *WS* (West Strip): - Normal Mode: previous page. - Search Mode: previous results page. -- *MB* (Middle Band): toggle the top and bottom bars. -- *RE* (Right Ear): +- *ES* (East Strip): - Normal Mode: next page. - Search Mode: next results page. -- *TL* (Top Left Corner): previous location. -- *TR* (Top Right Corner): toggle bookmark. -- *BL* (Bottom Left Corner): table of contents in normal mode, previous page in search mode. -- *BR* (Bottom Right Corner): go to page in normal mode, next page in search mode. +- *CR* (Center Rectangle), *SS* (South Strip) and *NS* (North Strip): toggle the top and bottom bars. +- *NWC* (North-West Corner): previous location. +- *NEC* (North-East Corner): toggle bookmark. +- *SWC* (South-West Corner): table of contents in normal mode, previous page in search mode. +- *SEC* (South-East Corner): go to page in normal mode, next page in search mode. Swipe west/east to go to the next/previous page. diff --git a/src/app.rs b/src/app.rs index bc29b4a..a395083 100644 --- a/src/app.rs +++ b/src/app.rs @@ -25,6 +25,7 @@ use crate::view::keyboard::{Layout}; use crate::view::dictionary::Dictionary as DictionaryApp; use crate::view::calculator::Calculator; use crate::view::sketch::Sketch; +use crate::view::touch_events::TouchEvents; use crate::document::sys_info_as_html; use crate::input::{DeviceEvent, PowerSource, ButtonCode, ButtonStatus, VAL_RELEASE, VAL_PRESS}; use crate::input::{raw_events, device_events, usb_events, display_rotate_event, button_scheme_event}; @@ -34,7 +35,7 @@ use crate::settings::{ButtonScheme, Settings, SETTINGS_PATH, RotationLock}; use crate::frontlight::{Frontlight, StandardFrontlight, NaturalFrontlight, PremixedFrontlight}; use crate::lightsensor::{LightSensor, KoboLightSensor}; use crate::battery::{Battery, KoboBattery}; -use crate::geom::{Rectangle, Edge}; +use crate::geom::{Rectangle, DiagDir, Region}; use crate::view::home::Home; use crate::view::reader::Reader; use crate::view::dialog::Dialog; @@ -780,20 +781,20 @@ pub fn run() -> Result<(), Error> { break; }, GestureEvent::MultiTap(mut points) => { - let mut rect = context.fb.rect(); - let w = rect.width() as i32; - let h = rect.height() as i32; - let m = w.min(h); - rect.shrink(&Edge::uniform(m / 12)); if points[0].x > points[1].x { points.swap(0, 1); } - if points[0].dist2(points[1]) >= rect.diag2() { - if points[0].y < points[1].y { - tx.send(Event::Select(EntryId::TakeScreenshot)).ok(); - } else { + let rect = context.fb.rect(); + let r1 = Region::from_point(points[0], rect, context.settings.strip_width, context.settings.corner_width); + let r2 = Region::from_point(points[1], rect, context.settings.strip_width, context.settings.corner_width); + match (r1, r2) { + (Region::Corner(DiagDir::SouthWest), Region::Corner(DiagDir::NorthEast)) => { rq.add(RenderData::new(view.id(), context.fb.rect(), UpdateMode::Full)); - } + }, + (Region::Corner(DiagDir::NorthWest), Region::Corner(DiagDir::SouthEast)) => { + tx.send(Event::Select(EntryId::TakeScreenshot)).ok(); + }, + _ => (), } }, _ => { @@ -882,6 +883,9 @@ pub fn run() -> Result<(), Error> { AppCmd::Calculator => Box::new(Calculator::new(context.fb.rect(), &tx, &mut rq, &mut context)?), AppCmd::Dictionary { ref query, ref language } => Box::new(DictionaryApp::new(context.fb.rect(), query, language, &tx, &mut rq, &mut context)), + AppCmd::TouchEvents => { + Box::new(TouchEvents::new(context.fb.rect(), &mut rq, &mut context)) + }, }; transfer_notifications(view.as_mut(), next_view.as_mut(), &mut rq, &mut context); history.push(HistoryItem { diff --git a/src/emulator.rs b/src/emulator.rs index 8a7cca8..0b64aa4 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -50,6 +50,7 @@ use crate::view::menu::{Menu, MenuKind}; use crate::view::dictionary::Dictionary; use crate::view::calculator::Calculator; use crate::view::sketch::Sketch; +use crate::view::touch_events::TouchEvents; use crate::view::common::{locate, locate_by_id, transfer_notifications, overlapping_rectangle}; use crate::view::common::{toggle_input_history_menu, toggle_keyboard_layout_menu}; use crate::helpers::{load_toml, save_toml}; @@ -292,6 +293,9 @@ fn main() -> Result<(), Error> { let rot = (5 + context.display.rotation) % 4; ty.send(DeviceEvent::RotateScreen(rot)).ok(); }, + Scancode::S => { + tx.send(Event::Select(EntryId::TakeScreenshot)).ok(); + }, _ => (), } }, @@ -351,6 +355,9 @@ fn main() -> Result<(), Error> { AppCmd::Dictionary { ref query, ref language } => { Box::new(Dictionary::new(context.fb.rect(), query, language, &tx, &mut rq, &mut context)) }, + AppCmd::TouchEvents => { + Box::new(TouchEvents::new(context.fb.rect(), &mut rq, &mut context)) + }, }; transfer_notifications(view.as_mut(), next_view.as_mut(), &mut rq, &mut context); history.push(view as Box); diff --git a/src/geom.rs b/src/geom.rs index 8b68cfb..5b6e50c 100644 --- a/src/geom.rs +++ b/src/geom.rs @@ -12,6 +12,17 @@ pub enum Dir { West, } +impl fmt::Display for Dir { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Dir::North => write!(f, "north"), + Dir::East => write!(f, "east"), + Dir::South => write!(f, "south"), + Dir::West => write!(f, "west"), + } + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum DiagDir { NorthWest, @@ -20,12 +31,32 @@ pub enum DiagDir { SouthWest, } +impl fmt::Display for DiagDir { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DiagDir::NorthWest => write!(f, "northwest"), + DiagDir::NorthEast => write!(f, "northeast"), + DiagDir::SouthEast => write!(f, "southeast"), + DiagDir::SouthWest => write!(f, "southwest"), + } + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum Axis { Horizontal, Vertical, } +impl fmt::Display for Axis { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Axis::Horizontal => write!(f, "horizontal"), + Axis::Vertical => write!(f, "vertical"), + } + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum CycleDir { Next, @@ -1185,6 +1216,80 @@ impl DivAssign for Boundary { } } +#[derive(Debug, Copy, Clone)] +pub enum Region { + Corner(DiagDir), + Strip(Dir), + Center, +} + +impl Region { + // pt ∈ rect + // 0.0 < {corner,strip}_width < 1.0 + pub fn from_point(pt: Point, rect: Rectangle, strip_width: f32, corner_width: f32) -> Region { + let w = rect.width() as i32; + let h = rect.height() as i32; + let m = w.min(h) as f32 / 2.0; + + let d = (m * corner_width).max(1.0) as i32; + let x1 = rect.min.x + d - 1; + let x2 = rect.max.x - d; + + // The four corners are on top of all the other regions. + if pt.x <= x1 { + let dx = x1 - pt.x; + if pt.y <= rect.min.y + dx { + return Region::Corner(DiagDir::NorthWest); + } else if pt.y >= rect.max.y - 1 - dx { + return Region::Corner(DiagDir::SouthWest); + } + } else if pt.x >= x2 { + let dx = pt.x - x2; + if pt.y <= rect.min.y + dx { + return Region::Corner(DiagDir::NorthEast); + } else if pt.y >= rect.max.y - 1 - dx { + return Region::Corner(DiagDir::SouthEast); + } + } + + let d = (m * strip_width).max(1.0) as i32; + let x1 = rect.min.x + d - 1; + let x2 = rect.max.x - d; + let y1 = rect.min.y + d - 1; + let y2 = rect.max.y - d; + + // The four strips are above the center region. + // Each of the diagonals between the strips has to belong to one of the strip. + if pt.x <= x1 { + let dx = pt.x - rect.min.x; + if pt.y >= rect.min.y + dx && pt.y < rect.max.y - 1 - dx { + return Region::Strip(Dir::West); + } + } else if pt.x >= x2 { + let dx = rect.max.x - 1 - pt.x; + if pt.y > rect.min.y + dx && pt.y <= rect.max.y - 1 - dx { + return Region::Strip(Dir::East); + } + } + + if pt.y <= y1 { + let dy = pt.y - rect.min.y; + if pt.x > rect.min.x + dy && pt.y <= rect.max.x - 1 - dy { + return Region::Strip(Dir::North); + } + } else if pt.y >= y2 { + let dy = rect.max.y - 1 - pt.y; + if pt.x >= rect.min.x + dy && pt.x < rect.max.x - 1 - dy { + return Region::Strip(Dir::South); + } + } + + // The center rectangle is below everything else. + Region::Center + } +} + + #[cfg(test)] mod tests { use super::{divide, LinearDir}; diff --git a/src/gesture.rs b/src/gesture.rs index 72f2667..1e37246 100644 --- a/src/gesture.rs +++ b/src/gesture.rs @@ -1,3 +1,4 @@ +use std::fmt; use std::sync::mpsc::{self, Sender, Receiver}; use std::sync::{Arc, Mutex}; use fxhash::FxHashMap; @@ -73,6 +74,29 @@ pub enum GestureEvent { HoldButtonLong(ButtonCode), } +impl fmt::Display for GestureEvent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + GestureEvent::Tap(pt) => write!(f, "Tap {}", pt), + GestureEvent::MultiTap(pts) => write!(f, "Multitap {} {}", pts[0], pts[1]), + GestureEvent::Swipe { dir, .. } => write!(f, "Swipe {}", dir), + GestureEvent::MultiSwipe { dir, .. } => write!(f, "Multiswipe {}", dir), + GestureEvent::Arrow { dir, .. } => write!(f, "Arrow {}", dir), + GestureEvent::MultiArrow { dir, .. } => write!(f, "Multiarrow {}", dir), + GestureEvent::Corner { dir, .. } => write!(f, "Corner {}", dir), + GestureEvent::MultiCorner { dir, .. } => write!(f, "Multicorner {}", dir), + GestureEvent::Pinch { axis, strength, .. } => write!(f, "Pinch {} {}", axis, strength), + GestureEvent::Spread { axis, strength, .. } => write!(f, "Spread {} {}", axis, strength), + GestureEvent::Rotate { center, quarter_turns, .. } => write!(f, "Rotate {} {}", center, quarter_turns * 90), + GestureEvent::Cross(pt) => write!(f, "Cross {}", pt), + GestureEvent::HoldFingerShort(pt, id) => write!(f, "Short-held finger {} {}", id, pt), + GestureEvent::HoldFingerLong(pt, id) => write!(f, "Long-held finger {} {}", id, pt), + GestureEvent::HoldButtonShort(code) => write!(f, "Short-held button {:?}", code), + GestureEvent::HoldButtonLong(code) => write!(f, "Long-held button {:?}", code), + } + } +} + #[derive(Debug)] pub struct TouchState { time: f64, diff --git a/src/settings/mod.rs b/src/settings/mod.rs index e308a2e..15586d8 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -66,6 +66,8 @@ pub struct Settings { pub auto_power_off: u8, pub time_format: String, pub date_format: String, + pub strip_width: f32, + pub corner_width: f32, #[serde(skip_serializing_if = "Vec::is_empty")] pub libraries: Vec, #[serde(skip_serializing_if = "FxHashMap::is_empty")] @@ -374,6 +376,8 @@ impl Default for Settings { button_scheme: ButtonScheme::Natural, auto_suspend: 30, auto_power_off: 3, + strip_width: 0.6, + corner_width: 0.4, time_format: "%H:%M".to_string(), date_format: "%A, %B %-d, %Y".to_string(), intermission_images: FxHashMap::default(), diff --git a/src/view/common.rs b/src/view/common.rs index dbf4da3..20ace90 100644 --- a/src/view/common.rs +++ b/src/view/common.rs @@ -85,8 +85,10 @@ pub fn toggle_main_menu(view: &mut dyn View, rect: Rectangle, enable: Option 0 && center.y < self.rect.min.y + dc { - self.go_to_last_page(hub, rq, context); - // Bottom left corner. - } else if dc > 0 && center.y > self.rect.max.y - dc { - if self.search.is_none() { - if self.ephemeral && self.info.file.path == PathBuf::from(TOC_SCHEME) { - self.quit(context); - hub.send(Event::Back).ok(); - } else { - hub.send(Event::Show(ViewId::TableOfContents)).ok(); - } - } else { - self.go_to_neighbor(CycleDir::Previous, hub, rq, context); - } - // Left ear. - } else { - if self.search.is_none() { - self.go_to_neighbor(CycleDir::Previous, hub, rq, context); - } else { - self.go_to_results_neighbor(CycleDir::Previous, hub, rq, context); - } - } - } else if center.x > x2 { - let dc = center.x - sx2; - // Top right corner. - if dc > 0 && center.y < self.rect.min.y + dc { - self.toggle_bookmark(rq); - // Bottom right corner. - } else if dc > 0 && center.y > self.rect.max.y - dc { - if self.search.is_none() { - hub.send(Event::Toggle(ViewId::GoToPage)).ok(); - } else { - self.go_to_neighbor(CycleDir::Next, hub, rq, context); + match Region::from_point(center, self.rect, context.settings.strip_width, context.settings.corner_width) { + Region::Corner(diag_dir) => { + match diag_dir { + DiagDir::NorthWest => self.go_to_last_page(hub, rq, context), + DiagDir::NorthEast => self.toggle_bookmark(rq), + DiagDir::SouthEast => { + if self.search.is_none() { + hub.send(Event::Toggle(ViewId::GoToPage)).ok(); + } else { + self.go_to_neighbor(CycleDir::Next, hub, rq, context); + } + }, + DiagDir::SouthWest => { + if self.search.is_none() { + if self.ephemeral && self.info.file.path == PathBuf::from(TOC_SCHEME) { + self.quit(context); + hub.send(Event::Back).ok(); + } else { + hub.send(Event::Show(ViewId::TableOfContents)).ok(); + } + } else { + self.go_to_neighbor(CycleDir::Previous, hub, rq, context); + } + }, } - // Right ear. - } else { - if self.search.is_none() { - self.go_to_neighbor(CycleDir::Next, hub, rq, context); - } else { - self.go_to_results_neighbor(CycleDir::Next, hub, rq, context); + }, + Region::Strip(dir) => { + match dir { + Dir::West => { + if self.search.is_none() { + self.go_to_neighbor(CycleDir::Previous, hub, rq, context); + } else { + self.go_to_results_neighbor(CycleDir::Previous, hub, rq, context); + } + }, + Dir::East => { + if self.search.is_none() { + self.go_to_neighbor(CycleDir::Next, hub, rq, context); + } else { + self.go_to_results_neighbor(CycleDir::Next, hub, rq, context); + } + }, + Dir::South | Dir::North => self.toggle_bars(None, hub, rq, context), } - } - // Middle band. - } else { - self.toggle_bars(None, hub, rq, context); + }, + Region::Center => self.toggle_bars(None, hub, rq, context), } true diff --git a/src/view/touch_events/mod.rs b/src/view/touch_events/mod.rs new file mode 100644 index 0000000..54857d1 --- /dev/null +++ b/src/view/touch_events/mod.rs @@ -0,0 +1,123 @@ +use crate::device::CURRENT_DEVICE; +use crate::geom::{Rectangle, Region, Dir, CornerSpec}; +use crate::view::icon::Icon; +use crate::view::notification::Notification; +use crate::view::{View, Event, Hub, Bus, RenderQueue, RenderData}; +use crate::view::{ViewId, Id, ID_FEEDER}; +use crate::view::SMALL_BAR_HEIGHT; +use crate::framebuffer::{Framebuffer, UpdateMode}; +use crate::font::Fonts; +use crate::unit::scale_by_dpi; +use crate::color::{BLACK, WHITE, GRAY05, GRAY10}; +use crate::app::Context; + +pub struct TouchEvents { + id: Id, + rect: Rectangle, + children: Vec>, + strip_width: f32, + corner_width: f32, +} + +impl TouchEvents { + pub fn new(rect: Rectangle, rq: &mut RenderQueue, context: &mut Context) -> TouchEvents { + let id = ID_FEEDER.next(); + let mut children = Vec::new(); + let dpi = CURRENT_DEVICE.dpi; + let small_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32; + let dx = (rect.width() as i32 - small_height) / 2; + let dy = (rect.height() as i32 - small_height) / 3; + let icon_rect = rect![rect.min.x + dx, rect.min.y + dy, + rect.min.x + dx + small_height, rect.min.y + dy + small_height]; + let icon = Icon::new("back", + icon_rect, + Event::Back) + .corners(Some(CornerSpec::Uniform(small_height/2))); + children.push(Box::new(icon) as Box); + rq.add(RenderData::new(id, rect, UpdateMode::Full)); + let strip_width = context.settings.strip_width; + let corner_width = context.settings.corner_width; + TouchEvents { + id, + rect, + children, + strip_width, + corner_width, + } + } + +} + +impl View for TouchEvents { + fn handle_event(&mut self, evt: &Event, hub: &Hub, _bus: &mut Bus, rq: &mut RenderQueue, context: &mut Context) -> bool { + match *evt { + Event::Gesture(ge) => { + let notif = Notification::new(ViewId::LoadSketchNotif, ge.to_string(), hub, rq, context); + self.children.push(Box::new(notif) as Box); + true + }, + _ => false, + } + } + + fn render(&self, fb: &mut dyn Framebuffer, rect: Rectangle, _fonts: &mut Fonts) { + for x in rect.min.x..rect.max.x { + for y in rect.min.y..rect.max.y { + let color = match Region::from_point(pt!(x, y), self.rect, self.strip_width, self.corner_width) { + Region::Corner(..) => BLACK, + Region::Strip(Dir::West) | Region::Strip(Dir::East) => GRAY05, + Region::Strip(Dir::South) | Region::Strip(Dir::North) => GRAY10, + Region::Center => WHITE, + }; + fb.set_pixel(x as u32, y as u32, color); + } + } + } + + fn render_rect(&self, rect: &Rectangle) -> Rectangle { + rect.intersection(&self.rect) + .unwrap_or(self.rect) + } + + fn is_background(&self) -> bool { + true + } + + fn resize(&mut self, rect: Rectangle, hub: &Hub, rq: &mut RenderQueue, context: &mut Context) { + let dpi = CURRENT_DEVICE.dpi; + let small_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32; + let dx = (rect.width() as i32 - small_height) / 2; + let dy = (rect.height() as i32 - small_height) / 3; + let icon_rect = rect![rect.min.x + dx, rect.min.y + dy, + rect.min.x + dx + small_height, rect.min.y + dy + small_height]; + self.children[0].resize(icon_rect, hub, rq, context); + + // Floating windows. + for i in 1..self.children.len() { + self.children[i].resize(rect, hub, rq, context); + } + + self.rect = rect; + rq.add(RenderData::new(self.id, self.rect, UpdateMode::Full)); + } + + fn rect(&self) -> &Rectangle { + &self.rect + } + + fn rect_mut(&mut self) -> &mut Rectangle { + &mut self.rect + } + + fn children(&self) -> &Vec> { + &self.children + } + + fn children_mut(&mut self) -> &mut Vec> { + &mut self.children + } + + fn id(&self) -> Id { + self.id + } +} -- 2.51.2