diff --git a/Data/system/cursor.png b/Data/system/cursor.png
new file mode 100644
index 00000000..7f35bc39
Binary files /dev/null and b/Data/system/cursor.png differ
diff --git a/Data/system_sources/cursor.svg b/Data/system_sources/cursor.svg
new file mode 100644
index 00000000..f8f79e1c
--- /dev/null
+++ b/Data/system_sources/cursor.svg
@@ -0,0 +1,44 @@
+
+
diff --git a/Devices/lilygo-tdeck/Source/Init.cpp b/Devices/lilygo-tdeck/Source/Init.cpp
index 8b88c405..5646dac6 100644
--- a/Devices/lilygo-tdeck/Source/Init.cpp
+++ b/Devices/lilygo-tdeck/Source/Init.cpp
@@ -7,10 +7,6 @@
#include
#include
#include
-#include
-#include
-
-#include
static const auto LOGGER = tt::Logger("T-Deck");
@@ -89,21 +85,6 @@ bool initBoot() {
LOGGER.error("{} start failed", trackball->getName());
}
}
-
- // Backlight doesn't seem to turn on until toggled on and off from keyboard settings...
- // Or let the display and backlight sleep then wake it up.
- // Then it works fine...until reboot, then you need to toggle again.
- // The current keyboard firmware sets backlight duty to 0 on boot.
- // https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/firmware/T-Keyboard_Keyboard_ESP32C3_250620.bin
- // https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/examples/Keyboard_ESP32C3/Keyboard_ESP32C3.ino#L25
- // https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/examples/Keyboard_ESP32C3/Keyboard_ESP32C3.ino#L217
- auto kbSettings = tt::settings::keyboard::loadOrGetDefault();
- bool result = keyboardbacklight::setBrightness(kbSettings.backlightEnabled ? kbSettings.backlightBrightness : 0);
- if (!result) {
- LOGGER.warn("Failed to set keyboard backlight brightness");
- }
-
- trackball::setEnabled(kbSettings.trackballEnabled);
});
return true;
diff --git a/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp b/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp
index c59c1916..b2bd6a06 100644
--- a/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp
+++ b/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp
@@ -1,6 +1,8 @@
#include "Trackball.h"
+#include
#include
+#include
static const auto LOGGER = tt::Logger("Trackball");
@@ -8,19 +10,292 @@ namespace trackball {
static TrackballConfig g_config;
static lv_indev_t* g_indev = nullptr;
-static bool g_initialized = false;
-static bool g_enabled = true;
+static std::atomic g_initialized{false};
+static std::atomic g_enabled{true};
+static std::atomic g_mode{Mode::Encoder};
-// Track last GPIO states for edge detection
-static bool g_lastState[5] = {false, false, false, false, false};
+// Interrupt-driven position tracking (atomic for ISR safety)
+static std::atomic g_cursorX{160};
+static std::atomic g_cursorY{120};
+static std::atomic g_buttonPressed{false};
-static void read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
- if (!g_initialized || !g_enabled) {
- data->state = LV_INDEV_STATE_RELEASED;
- data->enc_diff = 0;
+// Encoder mode: accumulated diff since last read
+static std::atomic g_encoderDiff{0};
+
+// Sensitivity cached for ISR access (atomic for thread safety)
+static std::atomic g_encoderSensitivity{1}; // Steps per tick for encoder
+static std::atomic g_pointerSensitivity{10}; // Pixels per tick for pointer
+
+// Cursor object for pointer mode
+static lv_obj_t* g_cursor = nullptr;
+
+// Screen dimensions (T-Deck: 320x240)
+static constexpr int32_t SCREEN_WIDTH = 320;
+static constexpr int32_t SCREEN_HEIGHT = 240;
+
+static constexpr int32_t CURSOR_SIZE = 16;
+
+// ISR handler for trackball directions
+static void IRAM_ATTR trackball_isr_handler(void* arg) {
+ // Skip accumulating movement when disabled
+ if (!g_enabled.load(std::memory_order_relaxed)) {
return;
}
-
+
+ gpio_num_t pin = static_cast(reinterpret_cast(arg));
+
+ if (g_mode.load(std::memory_order_relaxed) == Mode::Pointer) {
+ // Pointer mode: update absolute position using atomic fetch_add/sub
+ // Clamping is done in read_cb to avoid race conditions
+ int32_t step = g_pointerSensitivity.load(std::memory_order_relaxed);
+ if (pin == g_config.pinRight) {
+ g_cursorX.fetch_add(step, std::memory_order_relaxed);
+ } else if (pin == g_config.pinLeft) {
+ g_cursorX.fetch_sub(step, std::memory_order_relaxed);
+ } else if (pin == g_config.pinUp) {
+ g_cursorY.fetch_sub(step, std::memory_order_relaxed);
+ } else if (pin == g_config.pinDown) {
+ g_cursorY.fetch_add(step, std::memory_order_relaxed);
+ }
+ } else {
+ // Encoder mode: accumulate diff
+ int32_t step = g_encoderSensitivity.load(std::memory_order_relaxed);
+ if (pin == g_config.pinRight || pin == g_config.pinDown) {
+ g_encoderDiff.fetch_add(step, std::memory_order_relaxed);
+ } else if (pin == g_config.pinLeft || pin == g_config.pinUp) {
+ g_encoderDiff.fetch_sub(step, std::memory_order_relaxed);
+ }
+ }
+}
+
+// ISR handler for button (any edge)
+static void IRAM_ATTR button_isr_handler(void* arg) {
+ // Read current button state (active low)
+ bool pressed = gpio_get_level(g_config.pinClick) == 0;
+ g_buttonPressed.store(pressed, std::memory_order_relaxed);
+}
+
+// Helper to clamp value to range
+static inline int32_t clamp(int32_t val, int32_t minVal, int32_t maxVal) {
+ if (val < minVal) return minVal;
+ if (val > maxVal) return maxVal;
+ return val;
+}
+
+static void read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
+ Mode currentMode = g_mode.load(std::memory_order_relaxed);
+
+ if (!g_initialized.load(std::memory_order_relaxed) || !g_enabled.load(std::memory_order_relaxed)) {
+ data->state = LV_INDEV_STATE_RELEASED;
+ if (currentMode == Mode::Encoder) {
+ data->enc_diff = 0;
+ } else {
+ // Clamp cursor position to screen bounds
+ int32_t x = clamp(g_cursorX.load(std::memory_order_relaxed), 0, SCREEN_WIDTH - CURSOR_SIZE - 1);
+ int32_t y = clamp(g_cursorY.load(std::memory_order_relaxed), 0, SCREEN_HEIGHT - CURSOR_SIZE - 1);
+ g_cursorX.store(x, std::memory_order_relaxed);
+ g_cursorY.store(y, std::memory_order_relaxed);
+ data->point.x = static_cast(x);
+ data->point.y = static_cast(y);
+ }
+ return;
+ }
+
+ if (currentMode == Mode::Encoder) {
+ // Read and reset accumulated encoder diff
+ int32_t diff = g_encoderDiff.exchange(0);
+ data->enc_diff = static_cast(clamp(diff, INT16_MIN, INT16_MAX));
+
+ if (diff != 0) {
+ lv_disp_trig_activity(nullptr);
+ }
+ } else {
+ // Pointer mode: read and clamp cursor position
+ int32_t x = clamp(g_cursorX.load(std::memory_order_relaxed), 0, SCREEN_WIDTH - CURSOR_SIZE - 1);
+ int32_t y = clamp(g_cursorY.load(std::memory_order_relaxed), 0, SCREEN_HEIGHT - CURSOR_SIZE - 1);
+
+ // Store clamped values back to prevent unbounded growth
+ g_cursorX.store(x, std::memory_order_relaxed);
+ g_cursorY.store(y, std::memory_order_relaxed);
+
+ data->point.x = static_cast(x);
+ data->point.y = static_cast(y);
+ }
+
+ // Button state (same for both modes)
+ bool pressed = g_buttonPressed.load(std::memory_order_relaxed);
+ data->state = pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
+
+ if (pressed) {
+ lv_disp_trig_activity(nullptr);
+ }
+}
+
+lv_indev_t* init(const TrackballConfig& config) {
+ if (g_initialized.load(std::memory_order_relaxed)) {
+ LOGGER.warn("Already initialized");
+ return g_indev;
+ }
+
+ g_config = config;
+
+ // Set default sensitivities if not specified
+ if (g_config.encoderSensitivity == 0) {
+ g_config.encoderSensitivity = 1;
+ }
+ if (g_config.pointerSensitivity == 0) {
+ g_config.pointerSensitivity = 10;
+ }
+ g_encoderSensitivity.store(g_config.encoderSensitivity, std::memory_order_relaxed);
+ g_pointerSensitivity.store(g_config.pointerSensitivity, std::memory_order_relaxed);
+
+ // Initialize cursor position to center
+ g_cursorX.store(SCREEN_WIDTH / 2, std::memory_order_relaxed);
+ g_cursorY.store(SCREEN_HEIGHT / 2, std::memory_order_relaxed);
+ g_encoderDiff.store(0, std::memory_order_relaxed);
+ g_buttonPressed.store(false, std::memory_order_relaxed);
+
+ // Configure direction pins as interrupt inputs (falling edge)
+ const gpio_num_t dirPins[4] = {
+ config.pinRight,
+ config.pinUp,
+ config.pinLeft,
+ config.pinDown
+ };
+
+ gpio_config_t io_conf = {};
+ io_conf.intr_type = GPIO_INTR_NEGEDGE; // Falling edge (active low)
+ io_conf.mode = GPIO_MODE_INPUT;
+ io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
+ io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
+
+ // Install GPIO ISR service (if not already installed)
+ static bool isr_service_installed = false;
+ if (!isr_service_installed) {
+ esp_err_t err = gpio_install_isr_service(ESP_INTR_FLAG_IRAM);
+ if (err == ESP_OK || err == ESP_ERR_INVALID_STATE) {
+ // ESP_ERR_INVALID_STATE means already installed, which is fine
+ isr_service_installed = true;
+ } else {
+ LOGGER.error("Failed to install GPIO ISR service: {}", esp_err_to_name(err));
+ return nullptr;
+ }
+ }
+
+ // Track added handlers for cleanup on failure
+ int handlersAdded = 0;
+
+ // Configure and attach ISR for direction pins
+ for (int i = 0; i < 4; i++) {
+ io_conf.pin_bit_mask = (1ULL << dirPins[i]);
+ esp_err_t err = gpio_config(&io_conf);
+ if (err != ESP_OK) {
+ LOGGER.error("Failed to configure GPIO {}: {}", static_cast(dirPins[i]), esp_err_to_name(err));
+ // Cleanup previously added handlers
+ for (int j = 0; j < handlersAdded; j++) {
+ gpio_isr_handler_remove(dirPins[j]);
+ }
+ return nullptr;
+ }
+
+ err = gpio_isr_handler_add(dirPins[i], trackball_isr_handler, reinterpret_cast(static_cast(dirPins[i])));
+ if (err != ESP_OK) {
+ LOGGER.error("Failed to add ISR for GPIO {}: {}", static_cast(dirPins[i]), esp_err_to_name(err));
+ // Cleanup previously added handlers
+ for (int j = 0; j < handlersAdded; j++) {
+ gpio_isr_handler_remove(dirPins[j]);
+ }
+ return nullptr;
+ }
+ handlersAdded++;
+ }
+
+ // Configure button pin (any edge for press/release detection)
+ io_conf.intr_type = GPIO_INTR_ANYEDGE;
+ io_conf.pin_bit_mask = (1ULL << config.pinClick);
+ esp_err_t err = gpio_config(&io_conf);
+ if (err != ESP_OK) {
+ LOGGER.error("Failed to configure button GPIO {}: {}", static_cast(config.pinClick), esp_err_to_name(err));
+ // Cleanup direction handlers
+ for (int i = 0; i < 4; i++) {
+ gpio_isr_handler_remove(dirPins[i]);
+ }
+ return nullptr;
+ }
+
+ err = gpio_isr_handler_add(config.pinClick, button_isr_handler, nullptr);
+ if (err != ESP_OK) {
+ LOGGER.error("Failed to add button ISR: {}", esp_err_to_name(err));
+ // Cleanup direction handlers
+ for (int i = 0; i < 4; i++) {
+ gpio_isr_handler_remove(dirPins[i]);
+ }
+ return nullptr;
+ }
+
+ // Read initial button state
+ g_buttonPressed.store(gpio_get_level(config.pinClick) == 0);
+
+ // Register as LVGL encoder input device for group navigation (default mode)
+ g_indev = lv_indev_create();
+ if (g_indev == nullptr) {
+ LOGGER.error("Failed to register LVGL input device");
+ // Cleanup ISR handlers on failure
+ const gpio_num_t pins[5] = {
+ config.pinRight, config.pinUp, config.pinLeft,
+ config.pinDown, config.pinClick
+ };
+ for (int i = 0; i < 5; i++) {
+ gpio_intr_disable(pins[i]);
+ gpio_isr_handler_remove(pins[i]);
+ }
+ return nullptr;
+ }
+
+ lv_indev_set_type(g_indev, LV_INDEV_TYPE_ENCODER);
+ lv_indev_set_read_cb(g_indev, read_cb);
+ g_initialized.store(true, std::memory_order_relaxed);
+ LOGGER.info("Initialized with interrupts (R:{} U:{} L:{} D:{} Click:{})",
+ static_cast(config.pinRight),
+ static_cast(config.pinUp),
+ static_cast(config.pinLeft),
+ static_cast(config.pinDown),
+ static_cast(config.pinClick));
+
+ return g_indev;
+}
+
+// Create cursor for pointer mode
+static void createCursor() {
+ if (g_cursor != nullptr || g_indev == nullptr) return;
+
+ g_cursor = lv_image_create(lv_layer_sys());
+ if (g_cursor != nullptr) {
+ lv_obj_remove_flag(g_cursor, LV_OBJ_FLAG_CLICKABLE);
+
+ // Set cursor image
+ lv_image_set_src(g_cursor, TT_ASSETS_UI_CURSOR);
+ lv_indev_set_cursor(g_indev, g_cursor);
+ LOGGER.debug("Cursor created");
+ }
+}
+
+// Destroy cursor when switching back to encoder mode
+static void destroyCursor() {
+ if (g_cursor == nullptr) return;
+
+ // Delete the cursor object - this automatically detaches it from the indev
+ lv_obj_delete(g_cursor);
+ g_cursor = nullptr;
+ LOGGER.debug("Cursor destroyed");
+}
+
+void deinit() {
+ if (!g_initialized.load(std::memory_order_relaxed)) return;
+
+ destroyCursor();
+
+ // Disable interrupts and remove ISR handlers
const gpio_num_t pins[5] = {
g_config.pinRight,
g_config.pinUp,
@@ -28,122 +303,96 @@ static void read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
g_config.pinDown,
g_config.pinClick
};
-
- // Read GPIO states and detect changes (active low with pull-up)
- bool currentStates[5];
- for (int i = 0; i < 5; i++) {
- currentStates[i] = gpio_get_level(pins[i]) == 0;
- }
-
- // Process directional inputs as encoder steps
- // Right/Down = positive diff (next item), Left/Up = negative diff (prev item)
- int16_t diff = 0;
-
- // Right pressed (rising edge)
- if (currentStates[0] && !g_lastState[0]) {
- diff += g_config.movementStep;
- }
- // Up pressed (rising edge)
- if (currentStates[1] && !g_lastState[1]) {
- diff -= g_config.movementStep;
- }
- // Left pressed (rising edge)
- if (currentStates[2] && !g_lastState[2]) {
- diff -= g_config.movementStep;
- }
- // Down pressed (rising edge)
- if (currentStates[3] && !g_lastState[3]) {
- diff += g_config.movementStep;
- }
-
- // Update last states
- for (int i = 0; i < 5; i++) {
- g_lastState[i] = currentStates[i];
- }
-
- // Update encoder diff and button state
- data->enc_diff = diff;
- data->state = currentStates[4] ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
-
- // Trigger activity for wake-on-trackball
- if (diff != 0 || currentStates[4]) {
- lv_disp_trig_activity(nullptr);
- }
-}
-lv_indev_t* init(const TrackballConfig& config) {
- if (g_initialized) {
- LOGGER.warn("Already initialized");
- return g_indev;
- }
-
- g_config = config;
-
- // Set default movement step if not specified
- if (g_config.movementStep == 0) {
- g_config.movementStep = 10;
- }
-
- // Configure all GPIO pins as inputs with pull-ups (active low)
- const gpio_num_t pins[5] = {
- config.pinRight,
- config.pinUp,
- config.pinLeft,
- config.pinDown,
- config.pinClick
- };
-
- gpio_config_t io_conf = {};
- io_conf.intr_type = GPIO_INTR_DISABLE;
- io_conf.mode = GPIO_MODE_INPUT;
- io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
- io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
-
for (int i = 0; i < 5; i++) {
- io_conf.pin_bit_mask = (1ULL << pins[i]);
- gpio_config(&io_conf);
- g_lastState[i] = gpio_get_level(pins[i]) == 0;
- }
-
- // Register as LVGL encoder input device for group navigation
- g_indev = lv_indev_create();
- lv_indev_set_type(g_indev, LV_INDEV_TYPE_ENCODER);
- lv_indev_set_read_cb(g_indev, read_cb);
-
- if (g_indev != nullptr) {
- g_initialized = true;
- LOGGER.info("Initialized as encoder (R:{} U:{} L:{} D:{} Click:{})",
- static_cast(config.pinRight),
- static_cast(config.pinUp),
- static_cast(config.pinLeft),
- static_cast(config.pinDown),
- static_cast(config.pinClick));
- } else {
- LOGGER.error("Failed to register LVGL input device");
+ gpio_intr_disable(pins[i]);
+ gpio_isr_handler_remove(pins[i]);
}
- return g_indev;
-}
-
-void deinit() {
if (g_indev) {
lv_indev_delete(g_indev);
g_indev = nullptr;
}
- g_initialized = false;
+
+ g_initialized.store(false, std::memory_order_relaxed);
+ g_mode.store(Mode::Encoder, std::memory_order_relaxed);
+ g_enabled.store(true, std::memory_order_relaxed);
LOGGER.info("Deinitialized");
}
-void setMovementStep(uint8_t step) {
- if (step > 0) {
- g_config.movementStep = step;
- LOGGER.debug("Movement step set to {}", step);
+void setEncoderSensitivity(uint8_t sensitivity) {
+ if (sensitivity > 0) {
+ // Only update the atomic - ISR reads from atomic, not g_config
+ g_encoderSensitivity.store(sensitivity, std::memory_order_relaxed);
+ LOGGER.debug("Encoder sensitivity set to {}", sensitivity);
+ }
+}
+
+void setPointerSensitivity(uint8_t sensitivity) {
+ if (sensitivity > 0) {
+ // Only update the atomic - ISR reads from atomic, not g_config
+ g_pointerSensitivity.store(sensitivity, std::memory_order_relaxed);
+ LOGGER.debug("Pointer sensitivity set to {}", sensitivity);
}
}
void setEnabled(bool enabled) {
- g_enabled = enabled;
+ g_enabled.store(enabled, std::memory_order_relaxed);
+
+ if (!enabled) {
+ // Clear accumulated state to prevent jumps on re-enable
+ g_encoderDiff.store(0, std::memory_order_relaxed);
+ }
+
+ // Hide/show cursor based on enabled state when in pointer mode
+ // Note: Must be called from LVGL thread (main thread) for thread safety
+ lv_obj_t* cursor = g_cursor; // Local copy to avoid race with setMode
+ if (cursor != nullptr) {
+ if (enabled) {
+ lv_obj_clear_flag(cursor, LV_OBJ_FLAG_HIDDEN);
+ } else {
+ lv_obj_add_flag(cursor, LV_OBJ_FLAG_HIDDEN);
+ }
+ }
+
LOGGER.info("{}", enabled ? "Enabled" : "Disabled");
}
+void setMode(Mode mode) {
+ // Note: Must be called from LVGL thread (main thread) for thread safety
+ if (!g_initialized.load(std::memory_order_relaxed) || g_indev == nullptr) {
+ LOGGER.warn("Cannot set mode - not initialized");
+ return;
+ }
+
+ if (g_mode.load(std::memory_order_relaxed) == mode) {
+ return;
+ }
+
+ g_mode.store(mode, std::memory_order_relaxed);
+
+ if (mode == Mode::Pointer) {
+ // Switch to pointer mode
+ lv_indev_set_type(g_indev, LV_INDEV_TYPE_POINTER);
+ createCursor();
+ if (!g_enabled.load(std::memory_order_relaxed) && g_cursor != nullptr) {
+ lv_obj_add_flag(g_cursor, LV_OBJ_FLAG_HIDDEN);
+ }
+ // Reset cursor to center when switching modes
+ g_cursorX.store(SCREEN_WIDTH / 2, std::memory_order_relaxed);
+ g_cursorY.store(SCREEN_HEIGHT / 2, std::memory_order_relaxed);
+ LOGGER.info("Switched to Pointer mode");
+ } else {
+ // Switch to encoder mode
+ destroyCursor();
+ lv_indev_set_type(g_indev, LV_INDEV_TYPE_ENCODER);
+ g_encoderDiff.store(0, std::memory_order_relaxed); // Reset encoder diff
+ LOGGER.info("Switched to Encoder mode");
+ }
+}
+
+Mode getMode() {
+ return g_mode.load(std::memory_order_relaxed);
+}
+
}
diff --git a/Devices/lilygo-tdeck/Source/Trackball/Trackball.h b/Devices/lilygo-tdeck/Source/Trackball/Trackball.h
index acfe9c29..4f67c9f8 100644
--- a/Devices/lilygo-tdeck/Source/Trackball/Trackball.h
+++ b/Devices/lilygo-tdeck/Source/Trackball/Trackball.h
@@ -5,16 +5,25 @@
namespace trackball {
+/**
+ * @brief Trackball operating mode
+ */
+enum class Mode {
+ Encoder, // Navigation via enc_diff (scroll wheel behavior)
+ Pointer // Mouse cursor via point.x/y
+};
+
/**
* @brief Trackball configuration structure
*/
struct TrackballConfig {
- gpio_num_t pinRight; // Right direction GPIO
- gpio_num_t pinUp; // Up direction GPIO
- gpio_num_t pinLeft; // Left direction GPIO
- gpio_num_t pinDown; // Down direction GPIO
- gpio_num_t pinClick; // Click/select button GPIO
- uint8_t movementStep; // Pixels to move per trackball event (default: 10)
+ gpio_num_t pinRight; // Right direction GPIO
+ gpio_num_t pinUp; // Up direction GPIO
+ gpio_num_t pinLeft; // Left direction GPIO
+ gpio_num_t pinDown; // Down direction GPIO
+ gpio_num_t pinClick; // Click/select button GPIO
+ uint8_t encoderSensitivity = 1; // Encoder mode: steps per tick
+ uint8_t pointerSensitivity = 10; // Pointer mode: pixels per tick
};
/**
@@ -30,10 +39,16 @@ lv_indev_t* init(const TrackballConfig& config);
void deinit();
/**
- * @brief Set movement step size
- * @param step Encoder steps per trackball event
+ * @brief Set encoder mode sensitivity
+ * @param sensitivity Steps per trackball tick (1-10, default: 1)
*/
-void setMovementStep(uint8_t step);
+void setEncoderSensitivity(uint8_t sensitivity);
+
+/**
+ * @brief Set pointer mode sensitivity
+ * @param sensitivity Pixels per trackball tick (1-10, default: 10)
+ */
+void setPointerSensitivity(uint8_t sensitivity);
/**
* @brief Enable or disable trackball input processing
@@ -41,4 +56,16 @@ void setMovementStep(uint8_t step);
*/
void setEnabled(bool enabled);
+/**
+ * @brief Set trackball operating mode
+ * @param mode Encoder or Pointer mode
+ */
+void setMode(Mode mode);
+
+/**
+ * @brief Get current trackball operating mode
+ * @return Current mode
+ */
+Mode getMode();
+
}
diff --git a/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp b/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp
index a9137779..41ef5b67 100644
--- a/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp
+++ b/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp
@@ -1,16 +1,31 @@
#include "KeyboardBacklight.h"
#include // Driver
#include
+#include
// TODO: Add Mutex and consider refactoring into a class
bool KeyboardBacklightDevice::start() {
if (initialized) {
return true;
}
-
+
// T-Deck uses I2C_NUM_0 for internal peripherals
initialized = keyboardbacklight::init(I2C_NUM_0);
- return initialized;
+ if (!initialized) {
+ return false;
+ }
+
+ // Backlight doesn't seem to turn on until toggled on and off from keyboard settings...
+ // Or let the display and backlight sleep then wake it up.
+ // Then it works fine...until reboot, then you need to toggle again.
+ // The current keyboard firmware sets backlight duty to 0 on boot.
+ // https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/firmware/T-Keyboard_Keyboard_ESP32C3_250620.bin
+ // https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/examples/Keyboard_ESP32C3/Keyboard_ESP32C3.ino#L25
+ // https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/examples/Keyboard_ESP32C3/Keyboard_ESP32C3.ino#L217
+ auto kbSettings = tt::settings::keyboard::loadOrGetDefault();
+ keyboardbacklight::setBrightness(kbSettings.backlightEnabled ? kbSettings.backlightBrightness : 0);
+
+ return true;
}
bool KeyboardBacklightDevice::stop() {
diff --git a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp
index 16b65f26..9b24f10f 100644
--- a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp
+++ b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp
@@ -1,28 +1,49 @@
#include "TrackballDevice.h"
#include // Driver
+#include
+#include
+#include
+
+static const auto LOGGER = tt::Logger("TrackballDevice");
bool TrackballDevice::start() {
if (initialized) {
return true;
}
-
+
// T-Deck trackball GPIO configuration from LilyGo reference
trackball::TrackballConfig config = {
- .pinRight = GPIO_NUM_2, // BOARD_TBOX_G02
- .pinUp = GPIO_NUM_3, // BOARD_TBOX_G01
- .pinLeft = GPIO_NUM_1, // BOARD_TBOX_G04
- .pinDown = GPIO_NUM_15, // BOARD_TBOX_G03
- .pinClick = GPIO_NUM_0, // BOARD_BOOT_PIN
- .movementStep = 1 // pixels per movement
+ .pinRight = GPIO_NUM_2, // BOARD_TBOX_G02
+ .pinUp = GPIO_NUM_3, // BOARD_TBOX_G01
+ .pinLeft = GPIO_NUM_1, // BOARD_TBOX_G04
+ .pinDown = GPIO_NUM_15, // BOARD_TBOX_G03
+ .pinClick = GPIO_NUM_0, // BOARD_BOOT_PIN
+ .encoderSensitivity = 1, // 1 step per tick for menu navigation
+ .pointerSensitivity = 10 // 10 pixels per tick for cursor movement
};
-
+
indev = trackball::init(config);
- if (indev != nullptr) {
- initialized = true;
- return true;
+ if (indev == nullptr) {
+ return false;
}
-
- return false;
+
+ initialized = true;
+
+ // Apply persisted trackball settings (requires LVGL lock for cursor manipulation)
+ auto tbSettings = tt::settings::trackball::loadOrGetDefault();
+ if (tt::lvgl::lock(100)) {
+ trackball::setMode(tbSettings.trackballMode == tt::settings::trackball::TrackballMode::Pointer
+ ? trackball::Mode::Pointer
+ : trackball::Mode::Encoder);
+ trackball::setEncoderSensitivity(tbSettings.encoderSensitivity);
+ trackball::setPointerSensitivity(tbSettings.pointerSensitivity);
+ trackball::setEnabled(tbSettings.trackballEnabled);
+ tt::lvgl::unlock();
+ } else {
+ LOGGER.warn("Failed to acquire LVGL lock for trackball settings");
+ }
+
+ return true;
}
bool TrackballDevice::stop() {
diff --git a/Tactility/Include/Tactility/Assets.h b/Tactility/Include/Tactility/Assets.h
index b3efa797..6e7a865f 100644
--- a/Tactility/Include/Tactility/Assets.h
+++ b/Tactility/Include/Tactility/Assets.h
@@ -10,6 +10,7 @@
// UI
#define TT_ASSETS_UI_SPINNER TT_ASSET("spinner.png")
+#define TT_ASSETS_UI_CURSOR TT_ASSET("cursor.png")
// App icons
#define TT_ASSETS_APP_ICON_FALLBACK TT_ASSET("app_icon_fallback_dark_mode.png")
diff --git a/Tactility/Include/Tactility/settings/KeyboardSettings.h b/Tactility/Include/Tactility/settings/KeyboardSettings.h
index c65dbafd..baab26d8 100644
--- a/Tactility/Include/Tactility/settings/KeyboardSettings.h
+++ b/Tactility/Include/Tactility/settings/KeyboardSettings.h
@@ -5,11 +5,10 @@
namespace tt::settings::keyboard {
struct KeyboardSettings {
- bool backlightEnabled;
- uint8_t backlightBrightness; // 0-255
- bool trackballEnabled;
- bool backlightTimeoutEnabled;
- uint32_t backlightTimeoutMs; // Timeout in milliseconds
+ bool backlightEnabled = true;
+ uint8_t backlightBrightness = 127; // 0-255
+ bool backlightTimeoutEnabled = true;
+ uint32_t backlightTimeoutMs = 60000; // Timeout in milliseconds
};
bool load(KeyboardSettings& settings);
diff --git a/Tactility/Include/Tactility/settings/TrackballSettings.h b/Tactility/Include/Tactility/settings/TrackballSettings.h
new file mode 100644
index 00000000..ff769a50
--- /dev/null
+++ b/Tactility/Include/Tactility/settings/TrackballSettings.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include
+
+namespace tt::settings::trackball {
+
+enum class TrackballMode : uint8_t {
+ Encoder = 0, // Scroll wheel navigation (default)
+ Pointer = 1 // Mouse cursor mode
+};
+
+struct TrackballSettings {
+ bool trackballEnabled = false;
+ TrackballMode trackballMode = TrackballMode::Encoder;
+ uint8_t encoderSensitivity = 1; // Steps per tick (1-10)
+ uint8_t pointerSensitivity = 10; // Pixels per tick (1-10)
+};
+
+bool load(TrackballSettings& settings);
+
+TrackballSettings loadOrGetDefault();
+
+TrackballSettings getDefault();
+
+bool save(const TrackballSettings& settings);
+
+}
diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp
index e6b0ed4a..6f30c6eb 100644
--- a/Tactility/Source/Tactility.cpp
+++ b/Tactility/Source/Tactility.cpp
@@ -103,6 +103,9 @@ namespace app {
namespace systeminfo { extern const AppManifest manifest; }
namespace timedatesettings { extern const AppManifest manifest; }
namespace timezone { extern const AppManifest manifest; }
+#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK)
+ namespace trackballsettings { extern const AppManifest manifest; }
+#endif
namespace usbsettings { extern const AppManifest manifest; }
namespace wifiapsettings { extern const AppManifest manifest; }
namespace wificonnect { extern const AppManifest manifest; }
@@ -146,6 +149,9 @@ static void registerInternalApps() {
addAppManifest(app::systeminfo::manifest);
addAppManifest(app::timedatesettings::manifest);
addAppManifest(app::timezone::manifest);
+#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK)
+ addAppManifest(app::trackballsettings::manifest);
+#endif
addAppManifest(app::wifiapsettings::manifest);
addAppManifest(app::wificonnect::manifest);
addAppManifest(app::wifimanage::manifest);
diff --git a/Tactility/Source/app/keyboard/KeyboardSettings.cpp b/Tactility/Source/app/keyboard/KeyboardSettings.cpp
index 075d1842..89bc1f9a 100644
--- a/Tactility/Source/app/keyboard/KeyboardSettings.cpp
+++ b/Tactility/Source/app/keyboard/KeyboardSettings.cpp
@@ -13,14 +13,21 @@ namespace keyboardbacklight {
bool setBrightness(uint8_t brightness);
}
-namespace trackball {
- void setEnabled(bool enabled);
-}
-
namespace tt::app::keyboardsettings {
constexpr auto* TAG = "KeyboardSettings";
+// Shared timeout values: 15s, 30s, 1m, 2m, 5m, Never (0)
+static constexpr uint32_t TIMEOUT_VALUES_MS[] = {15000, 30000, 60000, 120000, 300000, 0};
+static constexpr size_t TIMEOUT_DEFAULT_IDX = 2; // 1 minute
+
+static uint32_t timeoutMsToIndex(uint32_t ms) {
+ for (size_t i = 0; i < sizeof(TIMEOUT_VALUES_MS) / sizeof(TIMEOUT_VALUES_MS[0]); ++i) {
+ if (TIMEOUT_VALUES_MS[i] == ms) return static_cast(i);
+ }
+ return TIMEOUT_DEFAULT_IDX;
+}
+
static void applyKeyboardBacklight(bool enabled, uint8_t brightness) {
keyboardbacklight::setBrightness(enabled ? brightness : 0);
}
@@ -30,7 +37,6 @@ class KeyboardSettingsApp final : public App {
settings::keyboard::KeyboardSettings kbSettings;
bool updated = false;
lv_obj_t* switchBacklight = nullptr;
- lv_obj_t* switchTrackball = nullptr;
lv_obj_t* sliderBrightness = nullptr;
lv_obj_t* switchTimeoutEnable = nullptr;
lv_obj_t* timeoutDropdown = nullptr;
@@ -57,14 +63,6 @@ class KeyboardSettingsApp final : public App {
}
}
- static void onTrackballSwitch(lv_event_t* e) {
- auto* app = static_cast(lv_event_get_user_data(e));
- bool enabled = lv_obj_has_state(app->switchTrackball, LV_STATE_CHECKED);
- app->kbSettings.trackballEnabled = enabled;
- app->updated = true;
- trackball::setEnabled(enabled);
- }
-
static void onTimeoutEnableSwitch(lv_event_t* e) {
auto* app = static_cast(lv_event_get_user_data(e));
bool enabled = lv_obj_has_state(app->switchTimeoutEnable, LV_STATE_CHECKED);
@@ -83,17 +81,16 @@ class KeyboardSettingsApp final : public App {
auto* app = static_cast(lv_event_get_user_data(event));
auto* dropdown = static_cast(lv_event_get_target(event));
uint32_t idx = lv_dropdown_get_selected(dropdown);
- // Map dropdown index to ms: 0=15s,1=30s,2=1m,3=2m,4=5m,5=Never
- static const uint32_t values_ms[] = {15000, 30000, 60000, 120000, 300000, 0};
- if (idx < (sizeof(values_ms)/sizeof(values_ms[0]))) {
- app->kbSettings.backlightTimeoutMs = values_ms[idx];
- app->updated = true;
+ if (idx < (sizeof(TIMEOUT_VALUES_MS) / sizeof(TIMEOUT_VALUES_MS[0]))) {
+ app->kbSettings.backlightTimeoutMs = TIMEOUT_VALUES_MS[idx];
+ app->updated = true;
}
}
public:
void onShow(AppContext& app, lv_obj_t* parent) override {
kbSettings = settings::keyboard::loadOrGetDefault();
+ updated = false;
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
@@ -136,20 +133,6 @@ public:
if (!kbSettings.backlightEnabled) lv_obj_add_state(sliderBrightness, LV_STATE_DISABLED);
lv_obj_add_event_cb(sliderBrightness, onBrightnessChanged, LV_EVENT_VALUE_CHANGED, this);
- // Trackball toggle
- auto* tb_wrapper = lv_obj_create(main_wrapper);
- lv_obj_set_size(tb_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
- lv_obj_set_style_pad_all(tb_wrapper, 0, LV_STATE_DEFAULT);
- lv_obj_set_style_border_width(tb_wrapper, 0, LV_STATE_DEFAULT);
-
- auto* tb_label = lv_label_create(tb_wrapper);
- lv_label_set_text(tb_label, "Trackball");
- lv_obj_align(tb_label, LV_ALIGN_LEFT_MID, 0, 0);
- switchTrackball = lv_switch_create(tb_wrapper);
- if (kbSettings.trackballEnabled) lv_obj_add_state(switchTrackball, LV_STATE_CHECKED);
- lv_obj_align(switchTrackball, LV_ALIGN_RIGHT_MID, 0, 0);
- lv_obj_add_event_cb(switchTrackball, onTrackballSwitch, LV_EVENT_VALUE_CHANGED, this);
-
// Backlight timeout enable
auto* to_enable_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(to_enable_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
@@ -181,20 +164,7 @@ public:
lv_obj_set_style_border_width(timeoutDropdown, 1, LV_PART_MAIN);
lv_obj_add_event_cb(timeoutDropdown, onTimeoutChanged, LV_EVENT_VALUE_CHANGED, this);
// Initialize dropdown selection from settings
- uint32_t ms = kbSettings.backlightTimeoutMs;
- uint32_t idx = 2; // default 1 minute
- if (ms == 15000) idx = 0;
- else if (ms == 30000)
- idx = 1;
- else if (ms == 60000)
- idx = 2;
- else if (ms == 120000)
- idx = 3;
- else if (ms == 300000)
- idx = 4;
- else if (ms == 0)
- idx = 5;
- lv_dropdown_set_selected(timeoutDropdown, idx);
+ lv_dropdown_set_selected(timeoutDropdown, timeoutMsToIndex(kbSettings.backlightTimeoutMs));
if (!kbSettings.backlightTimeoutEnabled) {
lv_obj_add_state(timeoutDropdown, LV_STATE_DISABLED);
}
@@ -204,6 +174,7 @@ public:
if (updated) {
const auto copy = kbSettings;
getMainDispatcher().dispatch([copy]{ settings::keyboard::save(copy); });
+ updated = false;
}
}
};
diff --git a/Tactility/Source/app/trackball/TrackballSettings.cpp b/Tactility/Source/app/trackball/TrackballSettings.cpp
new file mode 100644
index 00000000..332060e7
--- /dev/null
+++ b/Tactility/Source/app/trackball/TrackballSettings.cpp
@@ -0,0 +1,219 @@
+#ifdef ESP_PLATFORM
+
+#include
+
+#include
+#include
+#include
+
+#include
+
+// Forward declare driver functions
+namespace trackball {
+ void setEnabled(bool enabled);
+ enum class Mode { Encoder, Pointer };
+ void setMode(Mode mode);
+ void setEncoderSensitivity(uint8_t sensitivity);
+ void setPointerSensitivity(uint8_t sensitivity);
+}
+
+namespace tt::app::trackballsettings {
+
+constexpr auto* TAG = "TrackballSettings";
+
+static trackball::Mode toDriverMode(settings::trackball::TrackballMode mode) {
+ switch (mode) {
+ case settings::trackball::TrackballMode::Encoder: return trackball::Mode::Encoder;
+ case settings::trackball::TrackballMode::Pointer: return trackball::Mode::Pointer;
+ }
+ return trackball::Mode::Encoder; // default
+}
+
+// Convert settings enum to dropdown index (dropdown order: Encoder=0, Pointer=1)
+static uint32_t modeToDropdownIndex(settings::trackball::TrackballMode mode) {
+ switch (mode) {
+ case settings::trackball::TrackballMode::Encoder: return 0;
+ case settings::trackball::TrackballMode::Pointer: return 1;
+ }
+ return 0; // default to Encoder
+}
+
+class TrackballSettingsApp final : public App {
+
+ settings::trackball::TrackballSettings tbSettings;
+ bool updated = false;
+ lv_obj_t* switchTrackball = nullptr;
+ lv_obj_t* trackballModeDropdown = nullptr;
+ lv_obj_t* encoderSensitivitySlider = nullptr;
+ lv_obj_t* pointerSensitivitySlider = nullptr;
+
+ static void onTrackballSwitch(lv_event_t* e) {
+ auto* app = static_cast(lv_event_get_user_data(e));
+ bool enabled = lv_obj_has_state(app->switchTrackball, LV_STATE_CHECKED);
+ app->tbSettings.trackballEnabled = enabled;
+ app->updated = true;
+ trackball::setEnabled(enabled);
+
+ // Enable/disable controls based on trackball state
+ if (enabled) {
+ if (app->trackballModeDropdown) lv_obj_clear_state(app->trackballModeDropdown, LV_STATE_DISABLED);
+ if (app->encoderSensitivitySlider) lv_obj_clear_state(app->encoderSensitivitySlider, LV_STATE_DISABLED);
+ if (app->pointerSensitivitySlider) lv_obj_clear_state(app->pointerSensitivitySlider, LV_STATE_DISABLED);
+ } else {
+ if (app->trackballModeDropdown) lv_obj_add_state(app->trackballModeDropdown, LV_STATE_DISABLED);
+ if (app->encoderSensitivitySlider) lv_obj_add_state(app->encoderSensitivitySlider, LV_STATE_DISABLED);
+ if (app->pointerSensitivitySlider) lv_obj_add_state(app->pointerSensitivitySlider, LV_STATE_DISABLED);
+ }
+ }
+
+ static void onTrackballModeChanged(lv_event_t* e) {
+ auto* app = static_cast(lv_event_get_user_data(e));
+ uint32_t selected = lv_dropdown_get_selected(app->trackballModeDropdown);
+
+ // Validate selection matches expected enum values (dropdown order: Encoder=0, Pointer=1)
+ settings::trackball::TrackballMode mode;
+ switch (selected) {
+ case 0: mode = settings::trackball::TrackballMode::Encoder; break;
+ case 1: mode = settings::trackball::TrackballMode::Pointer; break;
+ default: return; // Invalid selection, ignore
+ }
+
+ app->tbSettings.trackballMode = mode;
+ app->updated = true;
+
+ // Apply mode change immediately
+ trackball::setMode(toDriverMode(mode));
+ }
+
+ static void onEncoderSensitivityChanged(lv_event_t* e) {
+ auto* app = static_cast(lv_event_get_user_data(e));
+ int32_t value = lv_slider_get_value(app->encoderSensitivitySlider);
+ app->tbSettings.encoderSensitivity = static_cast(value);
+ app->updated = true;
+
+ // Apply immediately
+ trackball::setEncoderSensitivity(static_cast(value));
+ }
+
+ static void onPointerSensitivityChanged(lv_event_t* e) {
+ auto* app = static_cast(lv_event_get_user_data(e));
+ int32_t value = lv_slider_get_value(app->pointerSensitivitySlider);
+ app->tbSettings.pointerSensitivity = static_cast(value);
+ app->updated = true;
+
+ // Apply immediately
+ trackball::setPointerSensitivity(static_cast(value));
+ }
+
+public:
+ void onShow(AppContext& app, lv_obj_t* parent) override {
+ tbSettings = settings::trackball::loadOrGetDefault();
+ auto ui_scale = hal::getConfiguration()->uiScale;
+ updated = false;
+
+ lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
+ lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
+
+ lv_obj_t* toolbar = lvgl::toolbar_create(parent, app);
+
+ switchTrackball = lvgl::toolbar_add_switch_action(toolbar);
+ lv_obj_add_event_cb(switchTrackball, onTrackballSwitch, LV_EVENT_VALUE_CHANGED, this);
+ if (tbSettings.trackballEnabled) lv_obj_add_state(switchTrackball, LV_STATE_CHECKED);
+
+ auto* main_wrapper = lv_obj_create(parent);
+ lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
+ lv_obj_set_width(main_wrapper, LV_PCT(100));
+ lv_obj_set_flex_grow(main_wrapper, 1);
+
+ // Trackball mode dropdown
+ auto* tb_mode_wrapper = lv_obj_create(main_wrapper);
+ lv_obj_set_size(tb_mode_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
+ lv_obj_set_style_pad_all(tb_mode_wrapper, 0, LV_STATE_DEFAULT);
+ lv_obj_set_style_border_width(tb_mode_wrapper, 0, LV_STATE_DEFAULT);
+
+ auto* tb_mode_label = lv_label_create(tb_mode_wrapper);
+ lv_label_set_text(tb_mode_label, "Mode");
+ lv_obj_align(tb_mode_label, LV_ALIGN_LEFT_MID, 0, 0);
+
+ trackballModeDropdown = lv_dropdown_create(tb_mode_wrapper);
+ lv_dropdown_set_options(trackballModeDropdown, "Encoder\nPointer");
+ lv_obj_align(trackballModeDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
+ lv_obj_set_style_border_color(trackballModeDropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN);
+ lv_obj_set_style_border_width(trackballModeDropdown, 1, LV_PART_MAIN);
+ lv_dropdown_set_selected(trackballModeDropdown, modeToDropdownIndex(tbSettings.trackballMode));
+ lv_obj_add_event_cb(trackballModeDropdown, onTrackballModeChanged, LV_EVENT_VALUE_CHANGED, this);
+
+ // Disable dropdown if trackball is disabled
+ if (!tbSettings.trackballEnabled) {
+ lv_obj_add_state(trackballModeDropdown, LV_STATE_DISABLED);
+ }
+
+ // Encoder sensitivity slider
+ auto* enc_sens_wrapper = lv_obj_create(main_wrapper);
+ lv_obj_set_size(enc_sens_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
+ lv_obj_set_style_pad_hor(enc_sens_wrapper, 0, LV_STATE_DEFAULT);
+ lv_obj_set_style_border_width(enc_sens_wrapper, 0, LV_STATE_DEFAULT);
+ if (ui_scale != hal::UiScale::Smallest) {
+ lv_obj_set_style_pad_ver(enc_sens_wrapper, 4, LV_STATE_DEFAULT);
+ }
+
+ auto* enc_sens_label = lv_label_create(enc_sens_wrapper);
+ lv_label_set_text(enc_sens_label, "Encoder Speed");
+ lv_obj_align(enc_sens_label, LV_ALIGN_LEFT_MID, 0, 0);
+
+ encoderSensitivitySlider = lv_slider_create(enc_sens_wrapper);
+ lv_slider_set_range(encoderSensitivitySlider, 1, 10);
+ lv_slider_set_value(encoderSensitivitySlider, tbSettings.encoderSensitivity, LV_ANIM_OFF);
+ lv_obj_set_width(encoderSensitivitySlider, LV_PCT(50));
+ lv_obj_align(encoderSensitivitySlider, LV_ALIGN_RIGHT_MID, 0, 0);
+ lv_obj_add_event_cb(encoderSensitivitySlider, onEncoderSensitivityChanged, LV_EVENT_VALUE_CHANGED, this);
+
+ if (!tbSettings.trackballEnabled) {
+ lv_obj_add_state(encoderSensitivitySlider, LV_STATE_DISABLED);
+ }
+
+ // Pointer sensitivity slider
+ auto* ptr_sens_wrapper = lv_obj_create(main_wrapper);
+ lv_obj_set_size(ptr_sens_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
+ lv_obj_set_style_pad_hor(ptr_sens_wrapper, 0, LV_STATE_DEFAULT);
+ lv_obj_set_style_border_width(ptr_sens_wrapper, 0, LV_STATE_DEFAULT);
+ if (ui_scale != hal::UiScale::Smallest) {
+ lv_obj_set_style_pad_ver(ptr_sens_wrapper, 4, LV_STATE_DEFAULT);
+ }
+
+ auto* ptr_sens_label = lv_label_create(ptr_sens_wrapper);
+ lv_label_set_text(ptr_sens_label, "Pointer Speed");
+ lv_obj_align(ptr_sens_label, LV_ALIGN_LEFT_MID, 0, 0);
+
+ pointerSensitivitySlider = lv_slider_create(ptr_sens_wrapper);
+ lv_slider_set_range(pointerSensitivitySlider, 1, 10);
+ lv_slider_set_value(pointerSensitivitySlider, tbSettings.pointerSensitivity, LV_ANIM_OFF);
+ lv_obj_set_width(pointerSensitivitySlider, LV_PCT(50));
+ lv_obj_align(pointerSensitivitySlider, LV_ALIGN_RIGHT_MID, 0, 0);
+ lv_obj_add_event_cb(pointerSensitivitySlider, onPointerSensitivityChanged, LV_EVENT_VALUE_CHANGED, this);
+
+ if (!tbSettings.trackballEnabled) {
+ lv_obj_add_state(pointerSensitivitySlider, LV_STATE_DISABLED);
+ }
+ }
+
+ void onHide(TT_UNUSED AppContext& app) override {
+ if (updated) {
+ const auto copy = tbSettings;
+ getMainDispatcher().dispatch([copy]{ settings::trackball::save(copy); });
+ updated = false;
+ }
+ }
+};
+
+extern const AppManifest manifest = {
+ .appId = "TrackballSettings",
+ .appName = "Trackball",
+ .appIcon = TT_ASSETS_APP_ICON_SETTINGS,
+ .appCategory = Category::Settings,
+ .createApp = create
+};
+
+}
+
+#endif
diff --git a/Tactility/Source/settings/KeyboardSettings.cpp b/Tactility/Source/settings/KeyboardSettings.cpp
index 2f7a2697..af2811a6 100644
--- a/Tactility/Source/settings/KeyboardSettings.cpp
+++ b/Tactility/Source/settings/KeyboardSettings.cpp
@@ -9,7 +9,6 @@ namespace tt::settings::keyboard {
constexpr auto* SETTINGS_FILE = "/data/settings/keyboard.properties";
constexpr auto* KEY_BACKLIGHT_ENABLED = "backlightEnabled";
constexpr auto* KEY_BACKLIGHT_BRIGHTNESS = "backlightBrightness";
-constexpr auto* KEY_TRACKBALL_ENABLED = "trackballEnabled";
constexpr auto* KEY_BACKLIGHT_TIMEOUT_ENABLED = "backlightTimeoutEnabled";
constexpr auto* KEY_BACKLIGHT_TIMEOUT_MS = "backlightTimeoutMs";
@@ -21,15 +20,13 @@ bool load(KeyboardSettings& settings) {
auto bl_enabled = map.find(KEY_BACKLIGHT_ENABLED);
auto bl_brightness = map.find(KEY_BACKLIGHT_BRIGHTNESS);
- auto tb_enabled = map.find(KEY_TRACKBALL_ENABLED);
auto bl_timeout_enabled = map.find(KEY_BACKLIGHT_TIMEOUT_ENABLED);
auto bl_timeout_ms = map.find(KEY_BACKLIGHT_TIMEOUT_MS);
settings.backlightEnabled = (bl_enabled != map.end()) ? (bl_enabled->second == "1" || bl_enabled->second == "true" || bl_enabled->second == "True") : true;
settings.backlightBrightness = (bl_brightness != map.end()) ? static_cast(std::stoi(bl_brightness->second)) : 127;
- settings.trackballEnabled = (tb_enabled != map.end()) ? (tb_enabled->second == "1" || tb_enabled->second == "true" || tb_enabled->second == "True") : true;
settings.backlightTimeoutEnabled = (bl_timeout_enabled != map.end()) ? (bl_timeout_enabled->second == "1" || bl_timeout_enabled->second == "true" || bl_timeout_enabled->second == "True") : true;
- settings.backlightTimeoutMs = (bl_timeout_ms != map.end()) ? static_cast(std::stoul(bl_timeout_ms->second)) : 30000; // Default 30 seconds
+ settings.backlightTimeoutMs = (bl_timeout_ms != map.end()) ? static_cast(std::stoul(bl_timeout_ms->second)) : 60000; // Default 60 seconds
return true;
}
@@ -38,7 +35,6 @@ KeyboardSettings getDefault() {
return KeyboardSettings{
.backlightEnabled = true,
.backlightBrightness = 127,
- .trackballEnabled = true,
.backlightTimeoutEnabled = true,
.backlightTimeoutMs = 60000 // 60 seconds default
};
@@ -56,7 +52,6 @@ bool save(const KeyboardSettings& settings) {
std::map map;
map[KEY_BACKLIGHT_ENABLED] = settings.backlightEnabled ? "1" : "0";
map[KEY_BACKLIGHT_BRIGHTNESS] = std::to_string(settings.backlightBrightness);
- map[KEY_TRACKBALL_ENABLED] = settings.trackballEnabled ? "1" : "0";
map[KEY_BACKLIGHT_TIMEOUT_ENABLED] = settings.backlightTimeoutEnabled ? "1" : "0";
map[KEY_BACKLIGHT_TIMEOUT_MS] = std::to_string(settings.backlightTimeoutMs);
return file::savePropertiesFile(SETTINGS_FILE, map);
diff --git a/Tactility/Source/settings/TrackballSettings.cpp b/Tactility/Source/settings/TrackballSettings.cpp
new file mode 100644
index 00000000..7478b948
--- /dev/null
+++ b/Tactility/Source/settings/TrackballSettings.cpp
@@ -0,0 +1,86 @@
+#include
+#include
+
+#include