feat: port bluetooth to MTK toolkit
This commit is contained in:
+1
-1
@@ -15,4 +15,4 @@ programs/gui/icons/
|
|||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
programs/lib/bearssl
|
programs/lib/bearssl
|
||||||
ramdisk.tar
|
ramdisk.tar
|
||||||
PLANS/
|
PLANS/.mcp.json
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"montauk-workflow": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "/home/daniel-hammer/dev/montauk-workflow-env/bin/python",
|
||||||
|
"args": [
|
||||||
|
"/home/daniel-hammer/dev/montauk-workflow/mcp_server.py"
|
||||||
|
],
|
||||||
|
"env": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+355
-275
@@ -7,9 +7,8 @@
|
|||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
#include <montauk/heap.h>
|
#include <montauk/heap.h>
|
||||||
#include <gui/gui.hpp>
|
#include <gui/mtk.hpp>
|
||||||
#include <gui/standalone.hpp>
|
#include <gui/standalone.hpp>
|
||||||
#include <gui/truetype.hpp>
|
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -24,16 +23,14 @@ using namespace gui;
|
|||||||
|
|
||||||
static constexpr int WIN_W = 360;
|
static constexpr int WIN_W = 360;
|
||||||
static constexpr int WIN_H = 420;
|
static constexpr int WIN_H = 420;
|
||||||
static constexpr int FONT_SIZE = 18;
|
|
||||||
static constexpr int FONT_SIZE_SM = 14;
|
|
||||||
|
|
||||||
static constexpr int ROW_H = 56;
|
static constexpr int ROW_H = 56;
|
||||||
static constexpr int TAB_H = 36;
|
static constexpr int TAB_H = 36;
|
||||||
static constexpr int PAD = 16;
|
static constexpr int PAD = 16;
|
||||||
static constexpr int BTN_W = 90;
|
static constexpr int BTN_W = 90;
|
||||||
static constexpr int BTN_H = 28;
|
static constexpr int BTN_H = 28;
|
||||||
static constexpr int BTN_RAD = 6;
|
|
||||||
static constexpr int STATUS_H = 28;
|
static constexpr int STATUS_H = 28;
|
||||||
|
static constexpr int SCAN_BTN_W = 100;
|
||||||
|
static constexpr int SCROLL_STEP = 20;
|
||||||
|
|
||||||
static constexpr int MAX_SCAN = 16;
|
static constexpr int MAX_SCAN = 16;
|
||||||
static constexpr int MAX_CONNECTED = 8;
|
static constexpr int MAX_CONNECTED = 8;
|
||||||
@@ -47,21 +44,28 @@ static constexpr Color BORDER = Color::from_rgb(0xCC, 0xCC, 0xCC);
|
|||||||
static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||||
static constexpr Color GREEN = Color::from_rgb(0x2E, 0xA0, 0x43);
|
static constexpr Color GREEN = Color::from_rgb(0x2E, 0xA0, 0x43);
|
||||||
static constexpr Color RED = Color::from_rgb(0xCC, 0x33, 0x33);
|
static constexpr Color RED = Color::from_rgb(0xCC, 0x33, 0x33);
|
||||||
static constexpr Color TAB_BG = Color::from_rgb(0xF5, 0xF5, 0xF5);
|
|
||||||
static constexpr Color TAB_ACTIVE = Color::from_rgb(0x36, 0x7B, 0xF0);
|
|
||||||
static constexpr Color TAB_INACTIVE = Color::from_rgb(0x66, 0x66, 0x66);
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// State
|
// State
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
enum Tab { TAB_DEVICES = 0, TAB_SCAN = 1 };
|
enum Tab {
|
||||||
|
TAB_DEVICES = 0,
|
||||||
|
TAB_SCAN = 1,
|
||||||
|
TAB_COUNT = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char* g_tab_labels[TAB_COUNT] = {
|
||||||
|
"Devices",
|
||||||
|
"Scan",
|
||||||
|
};
|
||||||
|
|
||||||
static TrueTypeFont* g_font = nullptr;
|
|
||||||
static int g_win_w = WIN_W;
|
static int g_win_w = WIN_W;
|
||||||
static int g_win_h = WIN_H;
|
static int g_win_h = WIN_H;
|
||||||
|
|
||||||
static Tab g_tab = TAB_DEVICES;
|
static Tab g_tab = TAB_DEVICES;
|
||||||
|
static int g_mouse_x = -1;
|
||||||
|
static int g_mouse_y = -1;
|
||||||
static int g_hover_row = -1;
|
static int g_hover_row = -1;
|
||||||
|
|
||||||
// Adapter info
|
// Adapter info
|
||||||
@@ -85,6 +89,142 @@ static int g_scroll = 0;
|
|||||||
static char g_status[80];
|
static char g_status[80];
|
||||||
static uint64_t g_status_time = 0;
|
static uint64_t g_status_time = 0;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Theme and layout
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static mtk::Theme bt_theme() {
|
||||||
|
mtk::Theme theme = mtk::make_theme(ACCENT);
|
||||||
|
theme.window_bg = BG_COLOR;
|
||||||
|
theme.surface = Color::from_rgb(0xF5, 0xF5, 0xF5);
|
||||||
|
theme.surface_alt = Color::from_rgb(0xF8, 0xF8, 0xF8);
|
||||||
|
theme.surface_hover = ROW_HOVER;
|
||||||
|
theme.border = BORDER;
|
||||||
|
theme.text = TEXT_COLOR;
|
||||||
|
theme.text_muted = Color::from_rgb(0x88, 0x88, 0x88);
|
||||||
|
theme.text_subtle = DIM_TEXT;
|
||||||
|
theme.selection = ACCENT;
|
||||||
|
theme.accent_soft = ROW_HOVER;
|
||||||
|
theme.danger = RED;
|
||||||
|
theme.danger_hover = mtk::darken(RED, 18);
|
||||||
|
theme.disabled_bg = Color::from_rgb(0xE8, 0xE8, 0xE8);
|
||||||
|
theme.disabled_fg = Color::from_rgb(0x9A, 0x9A, 0x9A);
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rect tab_bar_rect() {
|
||||||
|
return {0, 0, g_win_w, TAB_H};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rect content_rect() {
|
||||||
|
return {0, TAB_H, g_win_w, gui_max(g_win_h - TAB_H - STATUS_H, 0)};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rect status_bar_rect() {
|
||||||
|
return {0, g_win_h - STATUS_H, g_win_w, STATUS_H};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rect scan_button_rect() {
|
||||||
|
Rect content = content_rect();
|
||||||
|
return {(g_win_w - SCAN_BTN_W) / 2, content.y + 12, SCAN_BTN_W, BTN_H};
|
||||||
|
}
|
||||||
|
|
||||||
|
static int devices_list_top() {
|
||||||
|
return content_rect().y;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int devices_list_bottom() {
|
||||||
|
return status_bar_rect().y;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int scan_list_top() {
|
||||||
|
Rect scan_btn = scan_button_rect();
|
||||||
|
return scan_btn.y + scan_btn.h + 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int scan_list_bottom() {
|
||||||
|
return status_bar_rect().y;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rect devices_row_rect(int index) {
|
||||||
|
return {0, devices_list_top() + index * ROW_H - g_scroll, g_win_w, ROW_H};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rect scan_row_rect(int index) {
|
||||||
|
return {0, scan_list_top() + index * ROW_H - g_scroll, g_win_w, ROW_H};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rect row_action_button_rect(const Rect& row) {
|
||||||
|
return {g_win_w - PAD - BTN_W, row.y + (ROW_H - BTN_H) / 2, BTN_W, BTN_H};
|
||||||
|
}
|
||||||
|
|
||||||
|
static int current_row_count() {
|
||||||
|
return g_tab == TAB_DEVICES ? g_device_count : g_scan_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int current_list_top() {
|
||||||
|
return g_tab == TAB_DEVICES ? devices_list_top() : scan_list_top();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int current_list_bottom() {
|
||||||
|
return g_tab == TAB_DEVICES ? devices_list_bottom() : scan_list_bottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int max_scroll() {
|
||||||
|
int visible_h = gui_max(current_list_bottom() - current_list_top(), 0);
|
||||||
|
int total_h = current_row_count() * ROW_H;
|
||||||
|
if (total_h <= visible_h) return 0;
|
||||||
|
return total_h - visible_h;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clamp_scroll() {
|
||||||
|
g_scroll = gui_clamp(g_scroll, 0, max_scroll());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_text_fit(Canvas& c, int x, int y,
|
||||||
|
const char* text, int max_w, Color color) {
|
||||||
|
if (!text || !text[0] || max_w <= 4) return;
|
||||||
|
if (text_width(text) <= max_w) {
|
||||||
|
c.text(x, y, text, color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* ellipsis = "...";
|
||||||
|
int ell_w = text_width(ellipsis);
|
||||||
|
if (ell_w >= max_w) return;
|
||||||
|
|
||||||
|
char buf[96];
|
||||||
|
int out = 0;
|
||||||
|
while (text[out] && out < (int)sizeof(buf) - 4) {
|
||||||
|
buf[out] = text[out];
|
||||||
|
buf[out + 1] = '\0';
|
||||||
|
if (text_width(buf) + ell_w > max_w)
|
||||||
|
break;
|
||||||
|
out++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out <= 0) return;
|
||||||
|
buf[out] = '.';
|
||||||
|
buf[out + 1] = '.';
|
||||||
|
buf[out + 2] = '.';
|
||||||
|
buf[out + 3] = '\0';
|
||||||
|
c.text(x, y, buf, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_centered_text(Canvas& c, int area_y, int area_h, const char* text, Color color) {
|
||||||
|
int tw = text_width(text);
|
||||||
|
int th = system_font_height();
|
||||||
|
c.text((g_win_w - tw) / 2, area_y + (area_h - th) / 2, text, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool mouse_in_rect(const Rect& rect) {
|
||||||
|
return rect.contains(g_mouse_x, g_mouse_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
static mtk::WidgetState button_state(const Rect& rect, bool enabled, bool active = false) {
|
||||||
|
return mtk::widget_state(active, mouse_in_rect(rect), enabled);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Bluetooth helpers
|
// Bluetooth helpers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -104,7 +244,6 @@ static void refresh_devices() {
|
|||||||
g_device_count = montauk::bt_list(g_devices, MAX_CONNECTED);
|
g_device_count = montauk::bt_list(g_devices, MAX_CONNECTED);
|
||||||
if (g_device_count < 0) g_device_count = 0;
|
if (g_device_count < 0) g_device_count = 0;
|
||||||
|
|
||||||
// Resolve names from scan results or use address string
|
|
||||||
for (int i = 0; i < g_device_count; i++) {
|
for (int i = 0; i < g_device_count; i++) {
|
||||||
bool found = false;
|
bool found = false;
|
||||||
for (int j = 0; j < g_scan_count; j++) {
|
for (int j = 0; j < g_scan_count; j++) {
|
||||||
@@ -126,6 +265,8 @@ static void refresh_devices() {
|
|||||||
static void do_scan() {
|
static void do_scan() {
|
||||||
g_scanning = true;
|
g_scanning = true;
|
||||||
g_scan_count = 0;
|
g_scan_count = 0;
|
||||||
|
g_scroll = 0;
|
||||||
|
g_hover_row = -1;
|
||||||
set_status("Scanning...");
|
set_status("Scanning...");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +274,7 @@ static void finish_scan() {
|
|||||||
int r = montauk::bt_scan(g_scan, MAX_SCAN, 3000);
|
int r = montauk::bt_scan(g_scan, MAX_SCAN, 3000);
|
||||||
g_scan_count = r > 0 ? r : 0;
|
g_scan_count = r > 0 ? r : 0;
|
||||||
g_scanning = false;
|
g_scanning = false;
|
||||||
|
clamp_scroll();
|
||||||
char msg[64];
|
char msg[64];
|
||||||
snprintf(msg, sizeof(msg), "Found %d device%s", g_scan_count, g_scan_count == 1 ? "" : "s");
|
snprintf(msg, sizeof(msg), "Found %d device%s", g_scan_count, g_scan_count == 1 ? "" : "s");
|
||||||
set_status(msg);
|
set_status(msg);
|
||||||
@@ -140,22 +282,18 @@ static void finish_scan() {
|
|||||||
|
|
||||||
static void do_connect(const uint8_t* addr) {
|
static void do_connect(const uint8_t* addr) {
|
||||||
int r = montauk::bt_connect(addr);
|
int r = montauk::bt_connect(addr);
|
||||||
if (r >= 0) {
|
if (r >= 0) set_status("Connected");
|
||||||
set_status("Connected");
|
else set_status("Connection failed");
|
||||||
} else {
|
|
||||||
set_status("Connection failed");
|
|
||||||
}
|
|
||||||
refresh_devices();
|
refresh_devices();
|
||||||
|
clamp_scroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void do_disconnect(const uint8_t* addr) {
|
static void do_disconnect(const uint8_t* addr) {
|
||||||
int r = montauk::bt_disconnect(addr);
|
int r = montauk::bt_disconnect(addr);
|
||||||
if (r >= 0) {
|
if (r >= 0) set_status("Disconnected");
|
||||||
set_status("Disconnected");
|
else set_status("Disconnect failed");
|
||||||
} else {
|
|
||||||
set_status("Disconnect failed");
|
|
||||||
}
|
|
||||||
refresh_devices();
|
refresh_devices();
|
||||||
|
clamp_scroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool is_connected(const uint8_t* addr) {
|
static bool is_connected(const uint8_t* addr) {
|
||||||
@@ -196,164 +334,186 @@ static const char* rssi_bar(int8_t rssi) {
|
|||||||
return "Very weak";
|
return "Very weak";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void set_tab(Tab tab) {
|
||||||
|
if (tab == g_tab) return;
|
||||||
|
g_tab = tab;
|
||||||
|
g_scroll = 0;
|
||||||
|
g_hover_row = -1;
|
||||||
|
clamp_scroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Hover and hit testing
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static int get_hover_row(int mx, int my) {
|
||||||
|
if (g_tab == TAB_DEVICES) {
|
||||||
|
for (int i = 0; i < g_device_count; i++) {
|
||||||
|
if (devices_row_rect(i).contains(mx, my))
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < g_scan_count; i++) {
|
||||||
|
if (scan_row_rect(i).contains(mx, my))
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool handle_click(int mx, int my) {
|
||||||
|
int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, mx, my);
|
||||||
|
if (tab >= 0) {
|
||||||
|
set_tab((Tab)tab);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_tab == TAB_SCAN) {
|
||||||
|
Rect scan_btn = scan_button_rect();
|
||||||
|
if (!g_scanning && scan_btn.contains(mx, my)) {
|
||||||
|
do_scan();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < g_scan_count; i++) {
|
||||||
|
Rect row = scan_row_rect(i);
|
||||||
|
if (!row.contains(mx, my)) continue;
|
||||||
|
|
||||||
|
Rect action = row_action_button_rect(row);
|
||||||
|
if (action.contains(mx, my)) {
|
||||||
|
if (is_connected(g_scan[i].bdAddr))
|
||||||
|
do_disconnect(g_scan[i].bdAddr);
|
||||||
|
else
|
||||||
|
do_connect(g_scan[i].bdAddr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < g_device_count; i++) {
|
||||||
|
Rect row = devices_row_rect(i);
|
||||||
|
if (!row.contains(mx, my)) continue;
|
||||||
|
if (!g_devices[i].connected) continue;
|
||||||
|
|
||||||
|
Rect action = row_action_button_rect(row);
|
||||||
|
if (action.contains(mx, my)) {
|
||||||
|
do_disconnect(g_devices[i].bdAddr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Render
|
// Render
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
static void render(Canvas& canvas) {
|
static void render_devices_tab(Canvas& canvas, const mtk::Theme& theme) {
|
||||||
int W = g_win_w;
|
Rect list = {0, devices_list_top(), g_win_w, gui_max(devices_list_bottom() - devices_list_top(), 0)};
|
||||||
int H = g_win_h;
|
int fh = system_font_height();
|
||||||
|
|
||||||
// Background
|
|
||||||
canvas.fill(BG_COLOR);
|
|
||||||
|
|
||||||
// Tab bar
|
|
||||||
int tab_y = 0;
|
|
||||||
canvas.fill_rect(0, tab_y, W, TAB_H, TAB_BG);
|
|
||||||
canvas.hline(0, tab_y + TAB_H - 1, W, BORDER);
|
|
||||||
|
|
||||||
int tab_w = W / 2;
|
|
||||||
const char* tab_labels[] = { "Devices", "Scan" };
|
|
||||||
for (int i = 0; i < 2; i++) {
|
|
||||||
// Active tab gets white background to merge with content area
|
|
||||||
if (g_tab == i)
|
|
||||||
canvas.fill_rect(i * tab_w, tab_y, tab_w, TAB_H, BG_COLOR);
|
|
||||||
|
|
||||||
Color tc = (g_tab == i) ? TAB_ACTIVE : TAB_INACTIVE;
|
|
||||||
int tw = text_width(g_font, tab_labels[i], FONT_SIZE);
|
|
||||||
int tx = i * tab_w + (tab_w - tw) / 2;
|
|
||||||
draw_text(canvas, g_font, tx, tab_y + (TAB_H - text_height(g_font, FONT_SIZE)) / 2,
|
|
||||||
tab_labels[i], tc, FONT_SIZE);
|
|
||||||
// Active indicator underline
|
|
||||||
if (g_tab == i) {
|
|
||||||
canvas.fill_rect(i * tab_w + 4, tab_y + TAB_H - 3, tab_w - 8, 3, TAB_ACTIVE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int content_y = tab_y + TAB_H;
|
|
||||||
int content_h = H - content_y;
|
|
||||||
|
|
||||||
// Status bar at bottom
|
|
||||||
int list_h = content_h - STATUS_H;
|
|
||||||
|
|
||||||
if (g_tab == TAB_DEVICES) {
|
|
||||||
// Connected devices list
|
|
||||||
if (g_device_count == 0) {
|
if (g_device_count == 0) {
|
||||||
const char* msg = "No connected devices";
|
draw_centered_text(canvas, list.y, list.h, "No connected devices", theme.text_subtle);
|
||||||
int mw = text_width(g_font, msg, FONT_SIZE);
|
return;
|
||||||
draw_text(canvas, g_font,
|
}
|
||||||
(W - mw) / 2, content_y + list_h / 2 - text_height(g_font, FONT_SIZE) / 2,
|
|
||||||
msg, DIM_TEXT, FONT_SIZE);
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < g_device_count; i++) {
|
for (int i = 0; i < g_device_count; i++) {
|
||||||
int ry = content_y + i * ROW_H - g_scroll;
|
Rect row = devices_row_rect(i);
|
||||||
if (ry + ROW_H < content_y || ry >= content_y + list_h) continue;
|
if (row.y + row.h <= list.y || row.y >= list.y + list.h) continue;
|
||||||
|
|
||||||
// Hover highlight
|
|
||||||
if (g_hover_row == i)
|
if (g_hover_row == i)
|
||||||
canvas.fill_rect(0, ry, W, ROW_H, ROW_HOVER);
|
canvas.fill_rect(row.x, row.y, row.w, row.h, theme.surface_hover);
|
||||||
|
else
|
||||||
|
mtk::draw_list_row(canvas, row, false, (i & 1) != 0, theme);
|
||||||
|
|
||||||
// Connected indicator dot
|
Color dot_color = g_devices[i].connected ? GREEN : theme.text_subtle;
|
||||||
Color dot_c = g_devices[i].connected ? GREEN : DIM_TEXT;
|
fill_circle(canvas, PAD + 6, row.y + ROW_H / 2, 5, dot_color);
|
||||||
fill_circle(canvas, PAD + 6, ry + ROW_H / 2, 5, dot_c);
|
|
||||||
|
|
||||||
// Device name
|
Rect action = row_action_button_rect(row);
|
||||||
draw_text(canvas, g_font, PAD + 20, ry + 8, g_device_names[i], TEXT_COLOR, FONT_SIZE);
|
int text_x = PAD + 20;
|
||||||
|
int text_w_max = (g_devices[i].connected ? action.x : g_win_w - PAD) - text_x - 10;
|
||||||
|
draw_text_fit(canvas, text_x, row.y + 8, g_device_names[i], text_w_max, theme.text);
|
||||||
|
|
||||||
// Address below name
|
|
||||||
char addr_str[24];
|
char addr_str[24];
|
||||||
format_addr(addr_str, sizeof(addr_str), g_devices[i].bdAddr);
|
format_addr(addr_str, sizeof(addr_str), g_devices[i].bdAddr);
|
||||||
draw_text(canvas, g_font, PAD + 20, ry + 8 + text_height(g_font, FONT_SIZE) + 2,
|
draw_text_fit(canvas, text_x, row.y + 8 + fh + 2, addr_str, text_w_max, theme.text_subtle);
|
||||||
addr_str, DIM_TEXT, FONT_SIZE_SM);
|
|
||||||
|
|
||||||
// Disconnect button
|
|
||||||
if (g_devices[i].connected) {
|
if (g_devices[i].connected) {
|
||||||
int bx = W - PAD - BTN_W;
|
mtk::draw_button(canvas, action, "Disconnect", mtk::BUTTON_DANGER,
|
||||||
int by = ry + (ROW_H - BTN_H) / 2;
|
button_state(action, true), theme);
|
||||||
draw_button(canvas, g_font, bx, by, BTN_W, BTN_H,
|
|
||||||
"Disconnect", RED, WHITE, BTN_RAD, FONT_SIZE_SM);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Divider
|
mtk::draw_separator(canvas, PAD, row.y + ROW_H - 1, g_win_w - 2 * PAD, theme);
|
||||||
canvas.hline(PAD, ry + ROW_H - 1, W - 2 * PAD, BORDER);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Scan tab
|
static void render_scan_tab(Canvas& canvas, const mtk::Theme& theme) {
|
||||||
// Scan button at top of content area
|
Rect scan_btn = scan_button_rect();
|
||||||
int scan_btn_y = content_y + 12;
|
Rect list = {0, scan_list_top(), g_win_w, gui_max(scan_list_bottom() - scan_list_top(), 0)};
|
||||||
int scan_btn_w = 100;
|
int fh = system_font_height();
|
||||||
int scan_btn_x = (W - scan_btn_w) / 2;
|
|
||||||
if (g_scanning) {
|
if (g_scanning) {
|
||||||
draw_button(canvas, g_font, scan_btn_x, scan_btn_y, scan_btn_w, BTN_H,
|
mtk::draw_button(canvas, scan_btn, "Scanning...", mtk::BUTTON_SECONDARY,
|
||||||
"Scanning...", BORDER, DIM_TEXT, BTN_RAD, FONT_SIZE_SM);
|
button_state(scan_btn, false), theme);
|
||||||
} else {
|
} else {
|
||||||
draw_button(canvas, g_font, scan_btn_x, scan_btn_y, scan_btn_w, BTN_H,
|
mtk::draw_button(canvas, scan_btn, "Scan", mtk::BUTTON_PRIMARY,
|
||||||
"Scan", ACCENT, WHITE, BTN_RAD, FONT_SIZE_SM);
|
button_state(scan_btn, true), theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
int list_top = scan_btn_y + BTN_H + 12;
|
|
||||||
int scan_list_h = content_y + list_h - list_top;
|
|
||||||
|
|
||||||
if (g_scan_count == 0 && !g_scanning) {
|
if (g_scan_count == 0 && !g_scanning) {
|
||||||
const char* msg = "Press Scan to find devices";
|
draw_centered_text(canvas, list.y, list.h, "Press Scan to find devices", theme.text_subtle);
|
||||||
int mw = text_width(g_font, msg, FONT_SIZE);
|
return;
|
||||||
draw_text(canvas, g_font,
|
}
|
||||||
(W - mw) / 2, list_top + scan_list_h / 2 - text_height(g_font, FONT_SIZE) / 2,
|
|
||||||
msg, DIM_TEXT, FONT_SIZE);
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < g_scan_count; i++) {
|
for (int i = 0; i < g_scan_count; i++) {
|
||||||
int ry = list_top + i * ROW_H - g_scroll;
|
Rect row = scan_row_rect(i);
|
||||||
if (ry + ROW_H < list_top || ry >= list_top + scan_list_h) continue;
|
if (row.y + row.h <= list.y || row.y >= list.y + list.h) continue;
|
||||||
|
|
||||||
// Hover highlight
|
|
||||||
if (g_hover_row == i)
|
if (g_hover_row == i)
|
||||||
canvas.fill_rect(0, ry, W, ROW_H, ROW_HOVER);
|
canvas.fill_rect(row.x, row.y, row.w, row.h, theme.surface_hover);
|
||||||
|
else
|
||||||
|
mtk::draw_list_row(canvas, row, false, (i & 1) != 0, theme);
|
||||||
|
|
||||||
// Device type indicator
|
fill_circle(canvas, PAD + 6, row.y + ROW_H / 2, 5, theme.accent);
|
||||||
const char* type_str = device_class_str(g_scan[i].classOfDevice);
|
|
||||||
fill_circle(canvas, PAD + 6, ry + ROW_H / 2, 5, ACCENT);
|
Rect action = row_action_button_rect(row);
|
||||||
|
int text_x = PAD + 20;
|
||||||
|
int text_w_max = action.x - text_x - 10;
|
||||||
|
|
||||||
// Device name (or address if unnamed)
|
|
||||||
const char* display_name = g_scan[i].name[0] ? g_scan[i].name : "Unknown Device";
|
const char* display_name = g_scan[i].name[0] ? g_scan[i].name : "Unknown Device";
|
||||||
draw_text(canvas, g_font, PAD + 20, ry + 4, display_name, TEXT_COLOR, FONT_SIZE);
|
draw_text_fit(canvas, text_x, row.y + 4, display_name, text_w_max, theme.text);
|
||||||
|
|
||||||
// Type + signal below name
|
|
||||||
char detail[80];
|
char detail[80];
|
||||||
char addr_str[24];
|
char addr_str[24];
|
||||||
format_addr(addr_str, sizeof(addr_str), g_scan[i].bdAddr);
|
format_addr(addr_str, sizeof(addr_str), g_scan[i].bdAddr);
|
||||||
snprintf(detail, sizeof(detail), "%s | %s | %s",
|
snprintf(detail, sizeof(detail), "%s | %s | %s",
|
||||||
type_str, addr_str, rssi_bar(g_scan[i].rssi));
|
device_class_str(g_scan[i].classOfDevice), addr_str, rssi_bar(g_scan[i].rssi));
|
||||||
draw_text(canvas, g_font, PAD + 20, ry + 4 + text_height(g_font, FONT_SIZE) + 2,
|
draw_text_fit(canvas, text_x, row.y + 4 + fh + 2, detail, text_w_max, theme.text_subtle);
|
||||||
detail, DIM_TEXT, FONT_SIZE_SM);
|
|
||||||
|
|
||||||
// Connect/Disconnect button
|
|
||||||
bool conn = is_connected(g_scan[i].bdAddr);
|
bool conn = is_connected(g_scan[i].bdAddr);
|
||||||
int bx = W - PAD - BTN_W;
|
mtk::draw_button(canvas, action, conn ? "Disconnect" : "Connect",
|
||||||
int by = ry + (ROW_H - BTN_H) / 2;
|
conn ? mtk::BUTTON_DANGER : mtk::BUTTON_PRIMARY,
|
||||||
if (conn) {
|
button_state(action, true), theme);
|
||||||
draw_button(canvas, g_font, bx, by, BTN_W, BTN_H,
|
|
||||||
"Disconnect", RED, WHITE, BTN_RAD, FONT_SIZE_SM);
|
|
||||||
} else {
|
|
||||||
draw_button(canvas, g_font, bx, by, BTN_W, BTN_H,
|
|
||||||
"Connect", ACCENT, WHITE, BTN_RAD, FONT_SIZE_SM);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Divider
|
mtk::draw_separator(canvas, PAD, row.y + ROW_H - 1, g_win_w - 2 * PAD, theme);
|
||||||
canvas.hline(PAD, ry + ROW_H - 1, W - 2 * PAD, BORDER);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Status bar
|
static void render_status(Canvas& canvas, const mtk::Theme& theme) {
|
||||||
int status_y = H - STATUS_H;
|
Rect status = status_bar_rect();
|
||||||
canvas.fill_rect(0, status_y, W, STATUS_H, TAB_BG);
|
int sy = status.y + (status.h - system_font_height()) / 2;
|
||||||
canvas.hline(0, status_y, W, BORDER);
|
|
||||||
|
|
||||||
// Status bar content: adapter info on the left, status message on the right
|
|
||||||
uint64_t now = montauk::get_milliseconds();
|
uint64_t now = montauk::get_milliseconds();
|
||||||
int sy = status_y + (STATUS_H - text_height(g_font, FONT_SIZE_SM)) / 2;
|
|
||||||
|
|
||||||
|
canvas.fill_rect(status.x, status.y, status.w, status.h, theme.surface);
|
||||||
|
mtk::draw_separator(canvas, 0, status.y, g_win_w, theme);
|
||||||
|
|
||||||
|
int status_w = 0;
|
||||||
|
bool show_status = g_status[0] && (now - g_status_time) < 5000;
|
||||||
|
if (show_status)
|
||||||
|
status_w = text_width(g_status);
|
||||||
|
|
||||||
|
int left_max = g_win_w - PAD * 2 - (show_status ? status_w + 12 : 0);
|
||||||
if (g_adapter_ok) {
|
if (g_adapter_ok) {
|
||||||
char adapter_info[96];
|
char adapter_info[96];
|
||||||
char addr_str[24];
|
char addr_str[24];
|
||||||
@@ -361,109 +521,26 @@ static void render(Canvas& canvas) {
|
|||||||
const char* name = g_adapter.name[0] ? g_adapter.name : "Adapter";
|
const char* name = g_adapter.name[0] ? g_adapter.name : "Adapter";
|
||||||
snprintf(adapter_info, sizeof(adapter_info), "%s | %s | %d connected",
|
snprintf(adapter_info, sizeof(adapter_info), "%s | %s | %d connected",
|
||||||
name, addr_str, g_device_count);
|
name, addr_str, g_device_count);
|
||||||
draw_text(canvas, g_font, PAD, sy, adapter_info, DIM_TEXT, FONT_SIZE_SM);
|
draw_text_fit(canvas, PAD, sy, adapter_info, left_max, theme.text_subtle);
|
||||||
} else {
|
} else {
|
||||||
draw_text(canvas, g_font, PAD, sy, "No adapter found", RED, FONT_SIZE_SM);
|
draw_text_fit(canvas, PAD, sy, "No adapter found", left_max, theme.danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary status message (right-aligned, fades after 5s)
|
if (show_status)
|
||||||
if (g_status[0] && (now - g_status_time) < 5000) {
|
canvas.text(g_win_w - PAD - status_w, sy, g_status, theme.text_subtle);
|
||||||
int sw = text_width(g_font, g_status, FONT_SIZE_SM);
|
|
||||||
draw_text(canvas, g_font, W - PAD - sw, sy, g_status, DIM_TEXT, FONT_SIZE_SM);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
static void render(Canvas& canvas) {
|
||||||
// Hit testing
|
mtk::Theme theme = bt_theme();
|
||||||
// ============================================================================
|
canvas.fill(theme.window_bg);
|
||||||
|
mtk::draw_tab_bar(canvas, tab_bar_rect(), g_tab_labels, TAB_COUNT, g_tab, theme);
|
||||||
|
|
||||||
static bool handle_click(int mx, int my) {
|
if (g_tab == TAB_DEVICES)
|
||||||
int W = g_win_w;
|
render_devices_tab(canvas, theme);
|
||||||
|
else
|
||||||
|
render_scan_tab(canvas, theme);
|
||||||
|
|
||||||
// Tab bar
|
render_status(canvas, theme);
|
||||||
int tab_y = 0;
|
|
||||||
if (my >= tab_y && my < tab_y + TAB_H) {
|
|
||||||
Tab new_tab = (mx < W / 2) ? TAB_DEVICES : TAB_SCAN;
|
|
||||||
if (new_tab != g_tab) {
|
|
||||||
g_tab = new_tab;
|
|
||||||
g_scroll = 0;
|
|
||||||
g_hover_row = -1;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int content_y = tab_y + TAB_H;
|
|
||||||
|
|
||||||
if (g_tab == TAB_SCAN) {
|
|
||||||
// Scan button
|
|
||||||
int scan_btn_y = content_y + 8;
|
|
||||||
int scan_btn_w = 100;
|
|
||||||
int scan_btn_x = (W - scan_btn_w) / 2;
|
|
||||||
if (mx >= scan_btn_x && mx < scan_btn_x + scan_btn_w &&
|
|
||||||
my >= scan_btn_y && my < scan_btn_y + BTN_H && !g_scanning) {
|
|
||||||
do_scan();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan result rows
|
|
||||||
int list_top = scan_btn_y + BTN_H + 12;
|
|
||||||
for (int i = 0; i < g_scan_count; i++) {
|
|
||||||
int ry = list_top + i * ROW_H - g_scroll;
|
|
||||||
if (my >= ry && my < ry + ROW_H) {
|
|
||||||
// Check if disconnect/connect button clicked
|
|
||||||
int bx = W - PAD - BTN_W;
|
|
||||||
int by = ry + (ROW_H - BTN_H) / 2;
|
|
||||||
if (mx >= bx && mx < bx + BTN_W && my >= by && my < by + BTN_H) {
|
|
||||||
if (is_connected(g_scan[i].bdAddr)) {
|
|
||||||
do_disconnect(g_scan[i].bdAddr);
|
|
||||||
} else {
|
|
||||||
do_connect(g_scan[i].bdAddr);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Device rows
|
|
||||||
for (int i = 0; i < g_device_count; i++) {
|
|
||||||
int ry = content_y + i * ROW_H - g_scroll;
|
|
||||||
if (my >= ry && my < ry + ROW_H) {
|
|
||||||
// Disconnect button
|
|
||||||
if (g_devices[i].connected) {
|
|
||||||
int bx = W - PAD - BTN_W;
|
|
||||||
int by = ry + (ROW_H - BTN_H) / 2;
|
|
||||||
if (mx >= bx && mx < bx + BTN_W && my >= by && my < by + BTN_H) {
|
|
||||||
do_disconnect(g_devices[i].bdAddr);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int get_hover_row(int mx, int my) {
|
|
||||||
int content_y = TAB_H;
|
|
||||||
|
|
||||||
if (g_tab == TAB_DEVICES) {
|
|
||||||
int count = g_device_count;
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
int ry = content_y + i * ROW_H - g_scroll;
|
|
||||||
if (my >= ry && my < ry + ROW_H)
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
int list_top = content_y + 8 + BTN_H + 12;
|
|
||||||
for (int i = 0; i < g_scan_count; i++) {
|
|
||||||
int ry = list_top + i * ROW_H - g_scroll;
|
|
||||||
if (my >= ry && my < ry + ROW_H)
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -471,31 +548,23 @@ static int get_hover_row(int mx, int my) {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
extern "C" void _start() {
|
extern "C" void _start() {
|
||||||
// Load font
|
if (!fonts::init())
|
||||||
{
|
montauk::exit(1);
|
||||||
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
|
|
||||||
if (f) {
|
|
||||||
montauk::memset(f, 0, sizeof(TrueTypeFont));
|
|
||||||
if (!f->init("0:/fonts/Roboto-Medium.ttf")) { montauk::mfree(f); f = nullptr; }
|
|
||||||
}
|
|
||||||
g_font = f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query adapter
|
|
||||||
refresh_adapter();
|
refresh_adapter();
|
||||||
refresh_devices();
|
refresh_devices();
|
||||||
|
clamp_scroll();
|
||||||
g_status[0] = 0;
|
g_status[0] = 0;
|
||||||
|
|
||||||
WsWindow win;
|
WsWindow win;
|
||||||
if (!win.create("Bluetooth", WIN_W, WIN_H))
|
if (!win.create("Bluetooth", WIN_W, WIN_H))
|
||||||
montauk::exit(1);
|
montauk::exit(1);
|
||||||
|
|
||||||
Canvas canvas = win.canvas();
|
mtk::StandaloneHost host(&win);
|
||||||
|
Canvas canvas = host.canvas();
|
||||||
render(canvas);
|
render(canvas);
|
||||||
win.present();
|
host.present();
|
||||||
|
|
||||||
// Periodic refresh timer
|
|
||||||
uint64_t last_refresh = montauk::get_milliseconds();
|
uint64_t last_refresh = montauk::get_milliseconds();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -504,29 +573,30 @@ extern "C" void _start() {
|
|||||||
|
|
||||||
if (r < 0) break;
|
if (r < 0) break;
|
||||||
|
|
||||||
// Handle pending scan (non-blocking: kick it off after render)
|
|
||||||
if (g_scanning) {
|
if (g_scanning) {
|
||||||
canvas = win.canvas();
|
canvas = host.canvas();
|
||||||
render(canvas);
|
render(canvas);
|
||||||
win.present();
|
host.present();
|
||||||
finish_scan();
|
finish_scan();
|
||||||
refresh_devices();
|
refresh_devices();
|
||||||
canvas = win.canvas();
|
g_hover_row = get_hover_row(g_mouse_x, g_mouse_y);
|
||||||
|
canvas = host.canvas();
|
||||||
render(canvas);
|
render(canvas);
|
||||||
win.present();
|
host.present();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (r == 0) {
|
if (r == 0) {
|
||||||
// Periodic refresh every 5 seconds
|
|
||||||
uint64_t now = montauk::get_milliseconds();
|
uint64_t now = montauk::get_milliseconds();
|
||||||
if (now - last_refresh >= 5000) {
|
if (now - last_refresh >= 5000) {
|
||||||
refresh_adapter();
|
refresh_adapter();
|
||||||
refresh_devices();
|
refresh_devices();
|
||||||
|
clamp_scroll();
|
||||||
|
g_hover_row = get_hover_row(g_mouse_x, g_mouse_y);
|
||||||
last_refresh = now;
|
last_refresh = now;
|
||||||
canvas = win.canvas();
|
canvas = host.canvas();
|
||||||
render(canvas);
|
render(canvas);
|
||||||
win.present();
|
host.present();
|
||||||
}
|
}
|
||||||
montauk::sleep_ms(16);
|
montauk::sleep_ms(16);
|
||||||
continue;
|
continue;
|
||||||
@@ -534,52 +604,62 @@ extern "C" void _start() {
|
|||||||
|
|
||||||
bool redraw = false;
|
bool redraw = false;
|
||||||
|
|
||||||
if (ev.type == 3) break; // close
|
if (ev.type == 3) break;
|
||||||
|
|
||||||
// Resize
|
|
||||||
if (ev.type == 2) {
|
if (ev.type == 2) {
|
||||||
g_win_w = win.width;
|
g_win_w = win.width;
|
||||||
g_win_h = win.height;
|
g_win_h = win.height;
|
||||||
|
clamp_scroll();
|
||||||
|
g_hover_row = get_hover_row(g_mouse_x, g_mouse_y);
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard
|
|
||||||
if (ev.type == 0 && ev.key.pressed) {
|
if (ev.type == 0 && ev.key.pressed) {
|
||||||
if (ev.key.scancode == 0x01) break; // Escape
|
if (ev.key.scancode == 0x01) break;
|
||||||
if (ev.key.ascii == 's' || ev.key.ascii == 'S') {
|
if ((ev.key.ascii == 's' || ev.key.ascii == 'S') && !g_scanning) {
|
||||||
if (!g_scanning) { do_scan(); redraw = true; }
|
do_scan();
|
||||||
|
redraw = true;
|
||||||
|
}
|
||||||
|
if (ev.key.ascii == '1') {
|
||||||
|
set_tab(TAB_DEVICES);
|
||||||
|
redraw = true;
|
||||||
|
}
|
||||||
|
if (ev.key.ascii == '2') {
|
||||||
|
set_tab(TAB_SCAN);
|
||||||
|
redraw = true;
|
||||||
}
|
}
|
||||||
if (ev.key.ascii == '1') { g_tab = TAB_DEVICES; g_scroll = 0; redraw = true; }
|
|
||||||
if (ev.key.ascii == '2') { g_tab = TAB_SCAN; g_scroll = 0; redraw = true; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse
|
|
||||||
if (ev.type == 1) {
|
if (ev.type == 1) {
|
||||||
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
bool moved = (ev.mouse.x != g_mouse_x) || (ev.mouse.y != g_mouse_y);
|
||||||
|
g_mouse_x = ev.mouse.x;
|
||||||
|
g_mouse_y = ev.mouse.y;
|
||||||
|
|
||||||
int new_hover = get_hover_row(ev.mouse.x, ev.mouse.y);
|
int new_hover = get_hover_row(g_mouse_x, g_mouse_y);
|
||||||
if (new_hover != g_hover_row) {
|
if (new_hover != g_hover_row) {
|
||||||
g_hover_row = new_hover;
|
g_hover_row = new_hover;
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clicked) {
|
if (moved)
|
||||||
if (handle_click(ev.mouse.x, ev.mouse.y))
|
redraw = true;
|
||||||
|
|
||||||
|
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||||
|
if (clicked && handle_click(ev.mouse.x, ev.mouse.y))
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll
|
|
||||||
if (ev.mouse.scroll) {
|
if (ev.mouse.scroll) {
|
||||||
g_scroll -= ev.mouse.scroll * 20;
|
g_scroll -= ev.mouse.scroll * SCROLL_STEP;
|
||||||
if (g_scroll < 0) g_scroll = 0;
|
clamp_scroll();
|
||||||
|
g_hover_row = get_hover_row(g_mouse_x, g_mouse_y);
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redraw) {
|
if (redraw) {
|
||||||
canvas = win.canvas();
|
canvas = host.canvas();
|
||||||
render(canvas);
|
render(canvas);
|
||||||
win.present();
|
host.present();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user