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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dioxus-sdk-notification = { path = "packages/notification", version = "0.7.0" }
dioxus-sdk-sync = { path = "packages/sync", version = "0.7.0" }
dioxus-sdk-util = { path = "packages/util", version = "0.7.0" }
dioxus-sdk-window = { path = "packages/window", version = "0.7.0" }
dioxus-sdk-haptics = { path = "packages/haptics", version = "0.7.0" }

# Dioxus
dioxus = "0.7.0"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
- [x] Channels
- `dioxus-sdk-util`
- [x] `use_root_scroll`
- `dioxus-sdk-haptics` - Android & iOS
- [ ] Camera
- [ ] WiFi
- [ ] Bluetooth
Expand Down
15 changes: 15 additions & 0 deletions examples/haptics/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "haptics-example"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
dioxus = { workspace = true }
dioxus-sdk-haptics = { workspace = true }

[features]
default = ["android"]
android = []
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
11 changes: 11 additions & 0 deletions examples/haptics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# haptics

Learn how to use the `haptics` abstraction.

Run:

```dx serve --platorm android```

Or use the provided justfile to easily test on a real android device connected via ADB:

```just android-haptics```
33 changes: 33 additions & 0 deletions examples/haptics/justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
default:
@just --list

android-haptics device='':
#!/usr/bin/env bash
set -euo pipefail

serial='{{device}}'

adb start-server >/dev/null

if [[ -z "$serial" ]]; then
mapfile -t devices < <(adb devices | awk 'NR > 1 && $2 == "device" { print $1 }')

if [[ ${#devices[@]} -eq 0 ]]; then
echo 'No adb device is connected.' >&2
echo 'Connect an Android device over USB and make sure USB debugging is enabled.' >&2
exit 1
fi

if [[ ${#devices[@]} -gt 1 ]]; then
echo 'Multiple adb devices are connected. Pass a serial explicitly:' >&2
echo ' just android-haptics <serial>' >&2
adb devices -l >&2
exit 1
fi

serial="${devices[0]}"
fi

echo "Using adb device: $serial"
adb -s "$serial" wait-for-device
dx run --platform android --device "$serial"
111 changes: 111 additions & 0 deletions examples/haptics/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use dioxus::prelude::*;
use dioxus_sdk_haptics::{
impact_feedback, notification_feedback, selection_feedback, vibrate, ImpactFeedbackStyle,
NotificationFeedbackType,
};

fn main() {
launch(App);
}

#[component]
fn App() -> Element {
let status = use_signal(|| "Tap a button to test haptics.".to_string());

rsx! {
div {
style: "max-width: 720px; margin: 0 auto; padding: 24px; font-family: sans-serif;",
h1 { "Dioxus Haptics Example" }
p { "Run this on an mobile device to exercise the haptics bridge." }
p { strong { "Status: " } "{status}" }

section {
style: "margin-top: 24px;",
h2 { "Vibrate" }
div {
style: "display: flex; gap: 12px; flex-wrap: wrap;",
button {
onclick: move |_| run_action(status, "vibrate 25ms", map_result(vibrate(25))),
"25 ms"
}
button {
onclick: move |_| run_action(status, "vibrate 75ms", map_result(vibrate(75))),
"75 ms"
}
button {
onclick: move |_| run_action(status, "vibrate 150ms", map_result(vibrate(150))),
"150 ms"
}
}
}

section {
style: "margin-top: 24px;",
h2 { "Impact Feedback" }
div {
style: "display: flex; gap: 12px; flex-wrap: wrap;",
button {
onclick: move |_| run_action(status, "impact light", map_result(impact_feedback(ImpactFeedbackStyle::Light))),
"Light"
}
button {
onclick: move |_| run_action(status, "impact medium", map_result(impact_feedback(ImpactFeedbackStyle::Medium))),
"Medium"
}
button {
onclick: move |_| run_action(status, "impact heavy", map_result(impact_feedback(ImpactFeedbackStyle::Heavy))),
"Heavy"
}
button {
onclick: move |_| run_action(status, "impact soft", map_result(impact_feedback(ImpactFeedbackStyle::Soft))),
"Soft"
}
button {
onclick: move |_| run_action(status, "impact rigid", map_result(impact_feedback(ImpactFeedbackStyle::Rigid))),
"Rigid"
}
}
}

section {
style: "margin-top: 24px;",
h2 { "Notification Feedback" }
div {
style: "display: flex; gap: 12px; flex-wrap: wrap;",
button {
onclick: move |_| run_action(status, "notification success", map_result(notification_feedback(NotificationFeedbackType::Success))),
"Success"
}
button {
onclick: move |_| run_action(status, "notification warning", map_result(notification_feedback(NotificationFeedbackType::Warning))),
"Warning"
}
button {
onclick: move |_| run_action(status, "notification error", map_result(notification_feedback(NotificationFeedbackType::Error))),
"Error"
}
}
}

section {
style: "margin-top: 24px;",
h2 { "Selection" }
button {
onclick: move |_| run_action(status, "selection feedback", map_result(selection_feedback())),
"Selection"
}
}
}
}
}

fn run_action(mut status: Signal<String>, label: &'static str, result: Result<(), String>) {
match result {
Ok(()) => status.set(format!("Triggered {label}.")),
Err(err) => status.set(format!("{label} failed: {err}")),
}
}

fn map_result(result: Result<(), dioxus_sdk_haptics::HapticsError>) -> Result<(), String> {
result.map_err(|err| err.to_string())
}
25 changes: 25 additions & 0 deletions packages/haptics/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "dioxus-sdk-haptics"
version = "0.7.0"

description = "Haptics utilities for Dioxus."
readme = "./README.md"
keywords = ["gui", "dioxus"]
categories = ["gui", "android", "ios"]

edition.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true

[dependencies]
dioxus = { workspace = true }
cfg-if = { workspace = true }
serde = { workspace = true }

[target.'cfg(target_os = "android")'.dependencies]
manganis = "0.7.6"

[target.'cfg(target_os = "ios")'.dependencies]
manganis = "0.7.6"
43 changes: 43 additions & 0 deletions packages/haptics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Dioxus Haptics
Haptics utilities for Dioxus.

Heavily inspired by [tauri-plugin-haptics](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/haptics)

### Supports
- [ ] Web
- [ ] Windows
- [ ] Mac
- [ ] Linux
- [x] Android
- [x] iOs

## Usage
Add `dioxus-sdk-haptics` to your `Cargo.toml`:
```toml
[dependencies]
dioxus-sdk-haptics = "0.7"
```

Example:
```rs
use dioxus::prelude::*;
use dioxus_sdk_haptics::vibrate;

#[component]
fn App() -> Element {
let mut status = use_signal(|| "Tap the button to vibrate.".to_string());

rsx! {
div {
p { strong { "Status: " } "{status}" }
button {
onclick: move |_| match vibrate(25).map_err(|err| err.to_string()){
Ok(()) => status.set(format!("Triggered 25ms.")),
Err(err) => status.set(format!("25ms failed: {err}")),
},
"25 ms"
}
}
}
}
```
24 changes: 24 additions & 0 deletions packages/haptics/android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}

android {
namespace = "dev.dioxus.sdk.haptics"
compileSdk = 36

defaultConfig {
minSdk = 24
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}
}

dependencies {}
4 changes: 4 additions & 0 deletions packages/haptics/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.VIBRATE" />
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package dev.dioxus.sdk.haptics

import android.app.Activity
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import dev.dioxus.sdk.haptics.patterns.ImpactPatternHeavy
import dev.dioxus.sdk.haptics.patterns.ImpactPatternLight
import dev.dioxus.sdk.haptics.patterns.ImpactPatternMedium
import dev.dioxus.sdk.haptics.patterns.ImpactPatternRigid
import dev.dioxus.sdk.haptics.patterns.ImpactPatternSoft
import dev.dioxus.sdk.haptics.patterns.NotificationPatternError
import dev.dioxus.sdk.haptics.patterns.NotificationPatternSuccess
import dev.dioxus.sdk.haptics.patterns.NotificationPatternWarning
import dev.dioxus.sdk.haptics.patterns.Pattern
import dev.dioxus.sdk.haptics.patterns.SelectionPattern

class HapticsPlugin(activity: Activity) {
private val appContext = activity.applicationContext

private val vibrator: Vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager =
appContext.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
appContext.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}

fun vibrate(duration: Long) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(duration)
}
}

fun impactFeedback(style: String) {
vibratePattern(
when (style.lowercase()) {
"light" -> ImpactPatternLight
"heavy" -> ImpactPatternHeavy
"soft" -> ImpactPatternSoft
"rigid" -> ImpactPatternRigid
else -> ImpactPatternMedium
}
)
}

fun notificationFeedback(kind: String) {
vibratePattern(
when (kind.lowercase()) {
"warning" -> NotificationPatternWarning
"error" -> NotificationPatternError
else -> NotificationPatternSuccess
}
)
}

fun selectionFeedback() {
vibratePattern(SelectionPattern)
}

private fun vibratePattern(pattern: Pattern) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createWaveform(pattern.timings, pattern.amplitudes, -1))
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(pattern.oldSDKPattern, -1)
}
}
}
Loading
Loading