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:

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
+ }
+}