Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod db;
mod pointer_caps;
mod readany_cli;
mod storage;
mod sync;
Expand All @@ -10,6 +11,9 @@ use vector::VectorDBState;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// Must happen before the first WebView2 environment is created.
pointer_caps::apply_webview_pointer_capabilities();

tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
if let Some(window) = app.get_webview_window("main") {
Expand Down
122 changes: 122 additions & 0 deletions packages/app/src-tauri/src/pointer_caps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//! WebView2 pointer-capability correction for Windows touchscreen machines.
//!
//! Chromium's device enumeration on some Windows touchscreen devices (e.g.
//! Surface) registers only the touch digitizer, so CSS media queries report
//! `(hover: none)` and `(pointer: coarse)` even while a mouse is in use —
//! which keeps every Tailwind `hover:` / `group-hover:` utility (including
//! the tab close button) permanently disabled. The same applies to
//! `(any-hover)` / `(any-pointer)`, so no CSS-side query can recover.
//!
//! When a fine pointer (mouse or precision touchpad) is present we override
//! the renderer's report via blink-settings so the media queries match the
//! hardware. Must run before the first WebView2 environment is created.

const BLINK_SETTINGS_ARGS: &str =
"--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4";

#[cfg(target_os = "windows")]
pub fn apply_webview_pointer_capabilities() {
if !has_fine_pointer() {
return;
}

const KEY: &str = "WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS";
let mut value = std::env::var(KEY).unwrap_or_default();
if !value.is_empty() {
value.push(' ');
}
value.push_str(BLINK_SETTINGS_ARGS);
std::env::set_var(KEY, value);
}

#[cfg(not(target_os = "windows"))]
pub fn apply_webview_pointer_capabilities() {}

/// True when Windows reports a mouse or a precision touchpad.
///
/// Touchscreen digitizers deliberately do NOT count: Chromium treats the
/// primary pointer as touch there, which is exactly the report we correct.
#[cfg(target_os = "windows")]
fn has_fine_pointer() -> bool {
use std::alloc::{alloc, dealloc, Layout};

// Minimal FFI for user32 raw-input enumeration; avoids a new dependency.
#[link(name = "user32")]
extern "system" {
fn GetRawInputDeviceList(
list: *mut RawInputDeviceList,
count: *mut u32,
size: u32,
) -> u32;
fn GetRawInputDeviceInfoW(
device: isize,
command: u32,
data: *mut u8,
size: *mut u32,
) -> u32;
}

#[repr(C)]
#[derive(Clone, Copy)]
struct RawInputDeviceList {
h_device: isize,
dw_type: u32,
}

const RIM_TYPEMOUSE: u32 = 0;
const RIM_TYPEHID: u32 = 2;
const RIDI_DEVICEINFO: u32 = 0x2000_0007;
// HID usage-page / usage pairs from HID_USAGE_PAGE_DIGITIZER.
const USAGE_PAGE_DIGITIZER: u16 = 0x000D;
const USAGE_TOUCH_PAD: u16 = 0x0005;

unsafe {
let mut count: u32 = 0;
if GetRawInputDeviceList(std::ptr::null_mut(), &mut count, std::mem::size_of::<RawInputDeviceList>() as u32)
== u32::MAX
{
return false;
}
if count == 0 {
return false;
}
let layout = Layout::array::<RawInputDeviceList>(count as usize).expect("device list layout");
let list = alloc(layout) as *mut RawInputDeviceList;
if list.is_null() {
return false;
}
let fetched = GetRawInputDeviceList(list, &mut count, std::mem::size_of::<RawInputDeviceList>() as u32);

let mut fine = false;
if fetched != u32::MAX {
for i in 0..fetched as usize {
let device = *list.add(i);
if device.dw_type == RIM_TYPEMOUSE {
fine = true;
break;
}
if device.dw_type != RIM_TYPEHID {
continue;
}
// RID_DEVICE_INFO: cbSize@0, dwType@4, then the HID union
// member: vendorId@8, productId@10, versionNumber@12,
// usagePage@14, usage@16 (all USHORT).
let mut info = [0u8; 32];
let mut info_size = info.len() as u32;
if GetRawInputDeviceInfoW(device.h_device, RIDI_DEVICEINFO, info.as_mut_ptr(), &mut info_size)
!= u32::MAX
{
let usage_page = u16::from_ne_bytes([info[14], info[15]]);
let usage = u16::from_ne_bytes([info[16], info[17]]);
if usage_page == USAGE_PAGE_DIGITIZER && usage == USAGE_TOUCH_PAD {
fine = true;
break;
}
}
}
}

dealloc(list as *mut u8, layout);
fine
}
}
7 changes: 6 additions & 1 deletion packages/app/src/components/layout/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useReaderStore } from "@/stores/reader-store";
import { useSyncStore } from "@/stores/sync-store";
import { BookOpen, FilePenLine, Home, MessageSquare, NotebookPen, X } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";

const TAB_ICONS: Record<string, React.ElementType> = {
home: Home,
Expand Down Expand Up @@ -131,6 +132,7 @@ function TabItem({
onClose: () => void;
}) {
const Icon = TAB_ICONS[tab.type] ?? BookOpen;
const { t } = useTranslation();

return (
<div
Expand All @@ -147,7 +149,10 @@ function TabItem({
<span className="max-w-[120px] truncate">{tab.title}</span>
<button
type="button"
className="ml-0.5 hidden h-4 w-4 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-neutral-200/80 hover:text-foreground group-hover:flex"
title={t("tabs.close")}
// Always visible on the active tab — hover-gated elsewhere. Hover-only
// UI is unusable on devices whose webview reports (hover: none).
className={`ml-0.5 ${isActive ? "flex" : "hidden group-hover:flex"} h-4 w-4 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-neutral-200/80 hover:text-foreground`}
data-no-window-drag
onClick={(e) => { e.stopPropagation(); onClose(); }}
>
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"deleteGroupConfirm": "Delete this group? Books will not be deleted."
},
"tabs": {
"close": "Close tab",
"library": "Library",
"ai": "AI",
"notes": "Notes",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/i18n/locales/es/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"deleteGroupConfirm": "¿Eliminar este grupo? Los libros no se eliminarán."
},
"tabs": {
"close": "Cerrar pestaña",
"library": "Biblioteca",
"ai": "IA",
"notes": "Notas",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/i18n/locales/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"deleteGroupConfirm": "Supprimer ce groupe ? Les livres ne seront pas supprimés."
},
"tabs": {
"close": "Fermer l'onglet",
"library": "Bibliothèque",
"ai": "IA",
"notes": "Notes",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/i18n/locales/ja/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"deleteGroupConfirm": "このグループを削除しますか?書籍は削除されません。"
},
"tabs": {
"close": "タブを閉じる",
"library": "ライブラリ",
"ai": "AI",
"notes": "ノート",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/i18n/locales/ko/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"deleteGroupConfirm": "이 그룹을 삭제하시겠습니까? 책은 삭제되지 않습니다."
},
"tabs": {
"close": "탭 닫기",
"library": "서재",
"ai": "AI",
"notes": "노트",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/i18n/locales/zh-TW/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"deleteGroupConfirm": "確定刪除此分組?書籍不會被刪除。"
},
"tabs": {
"close": "關閉標籤頁",
"library": "書庫",
"ai": "AI",
"notes": "筆記",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/i18n/locales/zh/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"deleteGroupConfirm": "确定删除此分组?书籍不会被删除。"
},
"tabs": {
"close": "关闭标签页",
"library": "书库",
"ai": "AI",
"notes": "笔记",
Expand Down