Compare commits

...

17 Commits

Author SHA1 Message Date
eneller
2b6ea24d5e chore: bump version 2026-01-25 20:12:29 +01:00
eneller
02e6b033c2 chore: remove demo 2026-01-25 20:00:23 +01:00
eneller
076816bd12 doc: readme 2026-01-24 18:52:01 +01:00
eneller
1ad2e8ad4c doc: readme 2026-01-24 18:38:37 +01:00
eneller
3b5d304845 feat: working osk 2026-01-24 18:11:33 +01:00
eneller
cade0046bf doc: research 2025-12-17 20:18:51 +01:00
eneller
881a723f4e chore: update deps 2025-12-07 17:33:37 +01:00
eneller
673a71de43 Merge branch 'feature/keyboard-frontend' 2025-12-07 17:21:15 +01:00
eneller
6d16af7143 doc: README 2025-12-07 16:55:52 +01:00
lukasadrion
04a8a15f23 fix light and darkmode 2025-12-06 16:39:33 +01:00
lukasadrion
7744bcf67d remove german comments 2025-12-05 01:22:36 +01:00
lukasadrion
3512bf0fac finished circle keyboard implementation and update window size 2025-12-05 01:18:54 +01:00
lukasadrion
ef740ca9dd add first implementation of circle keyboard and better switching of keyboards 2025-12-04 18:54:10 +01:00
lukasadrion
15cdc77eea add DVORAK keyboard 2025-12-01 16:53:52 +01:00
lukasadrion
2ad8b7fffb remove initial html code 2025-12-01 16:40:31 +01:00
lukasadrion
327608cc36 add normal layout keyboard 2025-11-29 23:09:58 +01:00
eneller
6c4235a52f fix: linux-specific build 2025-11-28 14:09:55 +01:00
15 changed files with 3179 additions and 1557 deletions

2
.gitignore vendored
View File

@@ -40,3 +40,5 @@ testem.log
# System files
.DS_Store
Thumbs.db
TextTestExe/

View File

@@ -1,7 +1,20 @@
# Tauri + Angular
This template should help get you started developing with Tauri and Angular.
On Windows, Tauri requires the the msvc toolchain:
```
rustup default stable-msvc
```
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) + [Angular Language Service](https://marketplace.visualstudio.com/items?itemName=Angular.ng-template).
## Keyboard Emulation
Keyboard emulation on most systems requires workarounds for two issues:
- keeping the window with the keyboard on top while another (the input target) is in focus
- not receiving focus when clicked
- sending input
## Logging and Statistics
For logging, either use [TextTestExe](https://depts.washington.edu/acelab/proj/texttest/) (local program) or [TextTestPP](https://drustz.com/TextTestPP/) (online).
The theoretical framework is inspired by https://www.yorku.ca/mack/bit95.html

3966
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "keeb",
"version": "0.1.0",
"version": "1.0.1",
"scripts": {
"ng": "ng",
"start": "ng serve",
@@ -10,14 +10,14 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^17.0.0",
"@angular/common": "^17.0.0",
"@angular/compiler": "^17.0.0",
"@angular/core": "^17.0.0",
"@angular/forms": "^17.0.0",
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0",
"@angular/animations": "^18.2.14",
"@angular/common": "^18.2.14",
"@angular/compiler": "^18.2.14",
"@angular/core": "^18.2.14",
"@angular/forms": "^18.2.14",
"@angular/platform-browser": "^18.2.14",
"@angular/platform-browser-dynamic": "^18.2.14",
"@angular/router": "^18.2.14",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2",
@@ -25,9 +25,9 @@
"@tauri-apps/plugin-opener": "^2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.0",
"@angular/cli": "^17.0.0",
"@angular/compiler-cli": "^17.0.0",
"@angular-devkit/build-angular": "^18.2.21",
"@angular/cli": "^18.2.21",
"@angular/compiler-cli": "^18.2.14",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",

78
src-tauri/Cargo.lock generated
View File

@@ -840,7 +840,7 @@ dependencies = [
"objc2 0.6.3",
"objc2-app-kit",
"objc2-foundation 0.3.2",
"windows",
"windows 0.61.3",
"x11rb",
"xkbcommon",
"xkeysym",
@@ -1844,6 +1844,7 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-opener",
"windows 0.62.2",
"x11",
]
@@ -3598,7 +3599,7 @@ dependencies = [
"tao-macros",
"unicode-segmentation",
"url",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-version",
"x11-dl",
@@ -3670,7 +3671,7 @@ dependencies = [
"webkit2gtk",
"webview2-com",
"window-vibrancy",
"windows",
"windows 0.61.3",
]
[[package]]
@@ -3771,7 +3772,7 @@ dependencies = [
"tauri-plugin",
"thiserror 2.0.17",
"url",
"windows",
"windows 0.61.3",
"zbus",
]
@@ -3797,7 +3798,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.3",
]
[[package]]
@@ -3823,7 +3824,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.3",
"wry",
]
@@ -4550,7 +4551,7 @@ checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-implement",
"windows-interface",
@@ -4574,7 +4575,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
dependencies = [
"thiserror 2.0.17",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
]
@@ -4630,11 +4631,23 @@ version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections",
"windows-collections 0.2.0",
"windows-core 0.61.2",
"windows-future",
"windows-future 0.2.1",
"windows-link 0.1.3",
"windows-numerics",
"windows-numerics 0.2.0",
]
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections 0.3.2",
"windows-core 0.62.2",
"windows-future 0.3.2",
"windows-numerics 0.3.1",
]
[[package]]
@@ -4646,6 +4659,15 @@ dependencies = [
"windows-core 0.61.2",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core 0.62.2",
]
[[package]]
name = "windows-core"
version = "0.61.2"
@@ -4680,7 +4702,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link 0.1.3",
"windows-threading",
"windows-threading 0.1.0",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core 0.62.2",
"windows-link 0.2.1",
"windows-threading 0.2.1",
]
[[package]]
@@ -4727,6 +4760,16 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core 0.62.2",
"windows-link 0.2.1",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@@ -4856,6 +4899,15 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-version"
version = "0.1.7"
@@ -5082,7 +5134,7 @@ dependencies = [
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-version",
"x11-dl",

View File

@@ -23,9 +23,17 @@ tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
enigo = "0.6.1"
x11 = { version = "2.21.0", features = ["xlib"] }
raw-window-handle = "0.6.2"
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.62.2", features = [
"Win32_UI_WindowsAndMessaging",
"Win32_Foundation"
] }
[target.'cfg(target_os = "linux")'.dependencies]
x11 = { version = "2.21.0", features = ["xlib"] }
[profile.dev]
incremental = true # Compile your binary in smaller steps.

View File

@@ -1,25 +1,90 @@
use enigo::{Direction::Click, Enigo, Key, Keyboard, Settings};
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle, WindowHandle};
use std::ptr;
use enigo::{Direction, Enigo, Key, Keyboard, Settings};
use tauri::Manager;
#[cfg(target_os = "linux")]
use std::ptr;
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
let mut enigo = Enigo::new(&Settings::default()).unwrap();
enigo.key(Key::Unicode('a'), Click);
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[tauri::command]
fn send_key(key: String) -> Result<(), String> {
let mut enigo = Enigo::new(&Settings::default()).map_err(|e| format!("Enigo error: {}", e))?;
if key.len() == 1 {
let ch = key.chars().next().unwrap();
// Check if uppercase letter
if ch.is_uppercase() && ch.is_alphabetic() {
// Send Shift + lowercase letter
enigo.key(Key::Shift, Direction::Press)
.map_err(|e| format!("Shift press error: {}", e))?;
enigo.key(Key::Unicode(ch.to_lowercase().next().unwrap()), Direction::Click)
.map_err(|e| format!("Key error: {}", e))?;
enigo.key(Key::Shift, Direction::Release)
.map_err(|e| format!("Shift release error: {}", e))?;
} else {
// Send character as-is (lowercase or non-letter)
enigo.key(Key::Unicode(ch), Direction::Click)
.map_err(|e| format!("Key error: {}", e))?;
}
} else {
// Special keys
match key.as_str() {
"Enter" => enigo.key(Key::Return, Direction::Click)
.map_err(|e| format!("Key error: {}", e))?,
"Space" => enigo.key(Key::Space, Direction::Click)
.map_err(|e| format!("Key error: {}", e))?,
"Backspace" => enigo.key(Key::Backspace, Direction::Click)
.map_err(|e| format!("Key error: {}", e))?,
_ => return Err("Unknown key".to_string()),
}
}
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let window = app.get_webview_window("main").unwrap();
#[cfg(target_os = "windows")]
{
use windows::Win32::UI::WindowsAndMessaging::{SetWindowPos, HWND_TOPMOST, SWP_NOACTIVATE, GetWindowLongPtrW, SetWindowLongPtrW, GWL_EXSTYLE, WS_EX_NOACTIVATE};
use windows::Win32::Foundation::HWND;
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
unsafe {
if let Ok(RawWindowHandle::Win32(handle)) = window.raw_window_handle() {
let hwnd = HWND(handle.hwnd.get() as *mut core::ffi::c_void);
// Get current extended window style
let ex_style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE) as u32;
// Add WS_EX_NOACTIVATE flag
let new_style = ex_style | WS_EX_NOACTIVATE.0;
SetWindowLongPtrW(hwnd, GWL_EXSTYLE, new_style as isize);
// Position window as topmost without activating
/*
SetWindowPos(
hwnd,
Some(HWND_TOPMOST),
0, 0, 0, 0,
SWP_NOACTIVATE
); */
}
}
}
#[cfg(target_os = "linux")]
{
use x11::xlib;
// raw X11 pointers here
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
unsafe {
let handle = window.raw_window_handle();
if let Ok(RawWindowHandle::Xlib(h)) = handle {
@@ -28,7 +93,7 @@ pub fn run() {
let hints = xlib::XAllocWMHints();
if !hints.is_null() {
(*hints).flags = xlib::InputHint;
(*hints).input = 0; // Set input hint to False
(*hints).input = 0;
xlib::XSetWMHints(display, h.window, hints);
xlib::XFree(hints as *mut _);
}
@@ -37,10 +102,11 @@ pub fn run() {
}
}
}
Ok(())
})
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.invoke_handler(tauri::generate_handler![greet, send_key])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -14,7 +14,12 @@
{
"title": "keeb",
"width": 800,
"height": 600
"height": 700,
"minWidth": 800,
"minHeight": 700,
"resizable": true,
"center": true,
"alwaysOnTop": true
}
],
"security": {

View File

@@ -1,6 +1,4 @@
.logo.angular:hover {
filter: drop-shadow(0 0 2em #e32727);
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
@@ -33,10 +31,6 @@
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
@@ -109,4 +103,56 @@ button {
button:active {
background-color: #0f0f0f69;
}
.key-button {
background: rgb(58, 56, 56);
color: white;
}
}
.keyboard {
display: flex;
flex-direction: column;
gap: 8px;
padding: 20px;
background: #f5f5f5;
border-radius: 10px;
margin: 20px auto;
}
.keyboard-row {
display: flex;
gap: 6px;
justify-content: center;
}
.key-button {
min-width: 60px;
height: 60px;
font-size: 20px;
font-weight: bold;
border: 2px solid #ddd;
border-radius: 8px;
cursor: pointer;
transition: all 0.1s;
}
.key-button:hover {
background: #b1b1b1;
border-color: #3d3d3d;
}
.key-button:active {
background: #d0d0d0;
transform: scale(0.95);
}
.shift-key.active {
background: #4CAF50;
color: white;
border-color: #45a049;
}
.space-key {
min-width: 300px;
}

View File

@@ -1,19 +1,30 @@
<main class="container">
<h1>Welcome to Tauri + Angular!</h1>
<div class="row">
<a href="https://tauri.app" target="_blank">
<img src="/assets/tauri.svg" class="logo tauri" alt="Tauri logo" />
</a>
<a href="https://angular.io" target="_blank">
<img src="/assets/angular.svg" class="logo angular" alt="Angular logo" />
</a>
<!-- Layout Switcher -->
<div class="layout-controls">
<button (click)="switchLayout()" class="layout-button">
Switch Layout: {{ currentLayout.toUpperCase() }}
</button>
</div>
<p>Click on the logos to learn more about the frameworks</p>
<form class="row" (submit)="greet($event, greetInput.value)">
<input #greetInput id="greet-input" placeholder="Enter a name..." />
<button type="submit">Greet</button>
</form>
<p>{{ greetingMessage }}</p>
<!-- Conditional Rendering basierend auf currentLayout -->
<app-qwerty-keyboard
*ngIf="currentLayout === 'qwerty'"
[shiftActive]="shiftActive"
(keyPressed)="handleKeyPress($event)"
(shiftToggled)="toggleShift()">
</app-qwerty-keyboard>
<app-dvorak-keyboard
*ngIf="currentLayout === 'dvorak'"
[shiftActive]="shiftActive"
(keyPressed)="handleKeyPress($event)"
(shiftToggled)="toggleShift()">
</app-dvorak-keyboard>
<app-circle-keyboard
*ngIf="currentLayout === 'circle'"
[shiftActive]="shiftActive"
(keyPressed)="handleKeyPress($event)"
(shiftToggled)="toggleShift()">
</app-circle-keyboard>
</main>

View File

@@ -1,24 +1,56 @@
import { Component } from '@angular/core';
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { invoke } from "@tauri-apps/api/core";
import { QwertyKeyboardComponent } from './keyboards/qwerty-keyboard.component';
import { DvorakKeyboardComponent } from './keyboards/dvorak-keyboard.component';
import { CircleKeyboardComponent } from './keyboards/circle-keyboard.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet],
imports: [
CommonModule,
RouterOutlet,
QwertyKeyboardComponent,
DvorakKeyboardComponent,
CircleKeyboardComponent
],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
greetingMessage = "";
currentLayout: 'qwerty' | 'dvorak' | 'circle' = 'qwerty';
shiftActive = false;
greet(event: SubmitEvent, name: string): void {
event.preventDefault();
@ViewChild('greetInput') inputElement!: ElementRef;
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
invoke<string>("greet", { name }).then((text) => {
this.greetingMessage = text;
});
toggleShift(): void {
this.shiftActive = !this.shiftActive;
}
switchLayout(): void {
if (this.currentLayout == 'qwerty'){
this.currentLayout = 'dvorak';
} else if (this.currentLayout == 'dvorak'){
this.currentLayout = 'circle';
} else if (this.currentLayout == 'circle'){
this.currentLayout = 'qwerty';
}
}
async handleKeyPress(key: string): Promise<void> {
let finalKey = key;
if (key.length === 1) {
finalKey = this.shiftActive ? key.toUpperCase() : key.toLowerCase();
}
await invoke("send_key", { key: finalKey });
if (this.shiftActive && key.length === 1) {
this.shiftActive = false;
}
}
}

View File

@@ -0,0 +1,161 @@
.circle-keyboard-container {
position: relative;
width: 400px;
height: 400px;
margin: 50px auto;
background: #f5f5f5;
}
/* Center Button Container */
.center-button-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 140px;
height: 140px;
z-index: 1;
}
/* Center Button Sections */
.center-section {
position: absolute;
background: white;
color: #0f0f0f;
font-size: 20px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
border: 2px solid #ddd;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Top-left: Backspace (triangle) */
.top-left{
top: 0;
left: 0;
width: 70px;
height: 70px;
border-radius: 70px 0 0 0;
}
/* Top-right: Enter*/
.top-right {
top: 0;
right: 0;
width: 70px;
height: 70px;
border-radius: 0 70px 0 0;
}
/* Bottom: Space */
.bottom-left {
bottom: 0;
left: 0;
width: 70px;
height: 70px;
border-radius: 0 0 0 70px;
}
.bottom-left.active {
background: #4CAF50 !important;
color: white;
border-color: #45a049;
}
.bottom-right {
bottom: 0;
right: 0;
width: 70px;
height: 70px;
border-radius: 0 0 70px 0;
}
.middle-left {
top: 50%;
left: 50%;
transform: translate(-100%, -50%);
width: 30px;
height: 55px;
border-radius: 35px 0 0 35px;
z-index: 15;
}
.middle-right {
top: 50%;
left: 50%;
transform: translate(0%, -50%);
width: 30px;
height: 55px;
border-radius: 0 35px 35px 0;
z-index: 15;
}
/* 8 Circle Buttons */
.circle-button {
position: absolute;
top: 50%;
left: 50%;
width: 70px;
height: 70px;
margin: -35px 0 0 -35px;
border-radius: 50%;
border: 2px solid #ddd;
background: white;
color: #0f0f0f;
font-size: 24px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
box-shadow: 0 2px 2px rgba(0,0,0,0.2);
z-index: 1;
}
.second-ring {
width: 50px;
height: 50px;
font-size: 20px;
font-weight: normal;
margin: -25px 0 0 -25px;
}
.third-ring {
width: 40px;
height: 40px;
font-size: 18px;
font-weight: normal;
margin: -20px 0 0 -20px;
}
.fourth-ring {
width: 30px;
height: 30px;
font-size: 16px;
font-weight: lighter;
margin: -20px 0 0 -20px;
}
.circle-button:hover, .center-section:hover {
background: #b1b1b1;
border-color: #3d3d3d;
}
.circle-button:active, .center-section:active {
background: #d0d0d0;
}
@media (prefers-color-scheme: dark) {
.center-section {
background: rgb(58, 56, 56);
color: white;
}
.circle-button {
background: rgb(58, 56, 56);
color: white;
}
}

View File

@@ -0,0 +1,104 @@
import { Component, Output, EventEmitter, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
//TODO: Second and fourth ring of keys
@Component({
selector: 'app-circle-keyboard',
standalone: true,
imports: [CommonModule],
template: `
<div class="circle-keyboard-container">
<!-- Center Button - Large with 3 sections -->
<div class="center-button-container">
<!-- Top half split: Backspace left, Enter right -->
<button class="center-section top-left"
(click)="keyPressed.emit('Backspace')"
title="Backspace">
</button>
<button class="center-section top-right"
(click)="keyPressed.emit('Enter')"
title="Enter">
</button>
<!-- Bottom half: Space -->
<button class="center-section bottom-right"
(click)="keyPressed.emit('Space')"
title="Space">
</button>
<button (click)="shiftToggled.emit()"
[class.active]="shiftActive"
class="center-section bottom-left">
</button>
<button class="center-section middle-left"
(click)="keyPressed.emit('.')"
title="Period">
.
</button>
<button class="center-section middle-right"
(click)="keyPressed.emit(',')"
title="Comma">
,
</button>
</div>
<!-- 8 Surrounding Buttons in Circle -->
<button *ngFor="let key of circleKeysFirst; let i = index"
class="circle-button"
[style.transform]="getCirclePositionFromTop(i, 110)"
(click)="keyPressed.emit(key)">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
<button *ngFor="let key of circleKeysSecond; let i = index"
class="circle-button second-ring"
[style.transform]="getCirclePositionFromTop(i, 150, 22.5)"
(click)="keyPressed.emit(key)">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
<button *ngFor="let key of circleKeysThird; let i = index"
class="circle-button third-ring"
[style.transform]="getCirclePositionFromTop(i, 170)"
(click)="keyPressed.emit(key)">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
<button class="circle-button fourth-ring"
[style.transform]="getCirclePositionFromTop(0, 182, -12)"
(click)="keyPressed.emit(circleKeysFourth[0])">
{{ shiftActive ? circleKeysFourth[0] : circleKeysFourth[0].toLowerCase() }}
</button>
<button class="circle-button fourth-ring"
[style.transform]="getCirclePositionFromTop(0, 182, 14)"
(click)="keyPressed.emit(circleKeysFourth[1])">
{{ shiftActive ? circleKeysFourth[1] : circleKeysFourth[1].toLowerCase() }}
</button>
</div>
`,
styleUrl: './circle-keyboard.component.css'
})
export class CircleKeyboardComponent {
@Input() shiftActive = false;
@Output() keyPressed = new EventEmitter<string>();
@Output() shiftToggled = new EventEmitter<void>();
// 8 keys arranged in circle
circleKeysFirst = ['E', 'T', 'A', 'O', 'N', 'I', 'H', 'S'];
circleKeysSecond = ['R', 'L', 'D', 'U', 'C', 'M', 'W', 'Y'];
circleKeysThird = ['F', 'G', 'P', 'B', 'V', 'K', 'J', 'X'];
circleKeysFourth = ['Q', 'Z'];
// Calculate position for each button in circle
getCirclePositionFromTop(index: number, radius: number, offset: number = 0): string {
const angle = ((index * 45) - 90) + offset; // Start from top (0°), 45° apart
const angleRad = (angle * Math.PI) / 180;
const x = Math.cos(angleRad) * radius;
const y = Math.sin(angleRad) * radius;
return `translate(${x}px, ${y}px)`;
}
}

View File

@@ -0,0 +1,68 @@
import { Component, Output, EventEmitter, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-dvorak-keyboard',
standalone: true,
imports: [CommonModule],
template: `
<div class="keyboard">
<div class="keyboard-row">
<button *ngFor="let key of row1"
(click)="keyPressed.emit(key)"
class="key-button">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
</div>
<div class="keyboard-row">
<button *ngFor="let key of row2"
(click)="keyPressed.emit(key)"
class="key-button">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
</div>
<div class="keyboard-row">
<button *ngFor="let key of row3"
(click)="keyPressed.emit(key)"
class="key-button">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
</div>
<div class="keyboard-row special-keys">
<button (click)="shiftToggled.emit()"
[class.active]="shiftActive"
class="key-button shift-key">
⇧ Shift
</button>
<button (click)="keyPressed.emit('Space')"
class="key-button space-key">
Leertaste
</button>
<button (click)="keyPressed.emit('Backspace')"
class="key-button">
⌫ Delete
</button>
<button (click)="keyPressed.emit('Enter')"
class="key-button">
↵ Enter
</button>
</div>
</div>
`,
styleUrl: '../app.component.css'
})
export class DvorakKeyboardComponent {
@Input() shiftActive = false;
@Output() keyPressed = new EventEmitter<string>();
@Output() shiftToggled = new EventEmitter<void>();
row1 = [',', '.', 'P', 'Y', 'F', 'G', 'C', 'R', 'L'];
row2 = ['A', 'O', 'E', 'U', 'I', 'D', 'H', 'T', 'N', 'S'];
row3 = ['Q', 'J', 'K', 'X', 'B', 'M', 'W', 'V', 'Z'];
}

View File

@@ -0,0 +1,68 @@
import { Component, Output, EventEmitter, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-qwerty-keyboard',
standalone: true,
imports: [CommonModule],
template: `
<div class="keyboard">
<div class="keyboard-row">
<button *ngFor="let key of row1"
(click)="keyPressed.emit(key)"
class="key-button">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
</div>
<div class="keyboard-row">
<button *ngFor="let key of row2"
(click)="keyPressed.emit(key)"
class="key-button">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
</div>
<div class="keyboard-row">
<button *ngFor="let key of row3"
(click)="keyPressed.emit(key)"
class="key-button">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
</div>
<div class="keyboard-row special-keys">
<button (click)="shiftToggled.emit()"
[class.active]="shiftActive"
class="key-button shift-key">
⇧ Shift
</button>
<button (click)="keyPressed.emit('Space')"
class="key-button space-key">
Leertaste
</button>
<button (click)="keyPressed.emit('Backspace')"
class="key-button">
⌫ Delete
</button>
<button (click)="keyPressed.emit('Enter')"
class="key-button">
↵ Enter
</button>
</div>
</div>
`,
styleUrl: '../app.component.css'
})
export class QwertyKeyboardComponent {
@Input() shiftActive = false;
@Output() keyPressed = new EventEmitter<string>();
@Output() shiftToggled = new EventEmitter<void>();
row1 = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'];
row2 = ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'];
row3 = ['Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.'];
}