feat: Intel HDA audio driver, audio streaming syscalls, userspace Music app, fixes and improvements, rudimentary Bluetooth support
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# Makefile for bluetooth (standalone Bluetooth Manager) on MontaukOS
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
# ---- Toolchain ----
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
else
|
||||
CXX := g++
|
||||
endif
|
||||
|
||||
# ---- Paths ----
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
LIBDIR := ../../lib
|
||||
|
||||
# ---- Compiler flags ----
|
||||
|
||||
CXXFLAGS := \
|
||||
-std=gnu++20 \
|
||||
-g -O2 -pipe \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-Wno-unused-function \
|
||||
-nostdinc \
|
||||
-ffreestanding \
|
||||
-fno-stack-protector \
|
||||
-fno-stack-check \
|
||||
-fno-PIC \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-msse \
|
||||
-msse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
# ---- Linker flags ----
|
||||
|
||||
LDFLAGS := \
|
||||
-nostdlib \
|
||||
-static \
|
||||
-Wl,--build-id=none \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,-m,elf_x86_64 \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
# ---- Source files ----
|
||||
|
||||
SRCS := main.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := $(BINDIR)/apps/bluetooth/bluetooth.elf
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/apps/bluetooth
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,670 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS Bluetooth Manager
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int WIN_W = 360;
|
||||
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 TAB_H = 36;
|
||||
static constexpr int PAD = 16;
|
||||
static constexpr int BTN_W = 90;
|
||||
static constexpr int BTN_H = 28;
|
||||
static constexpr int BTN_RAD = 6;
|
||||
static constexpr int STATUS_H = 28;
|
||||
|
||||
static constexpr int MAX_SCAN = 16;
|
||||
static constexpr int MAX_CONNECTED = 8;
|
||||
|
||||
static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
static constexpr Color TEXT_COLOR = Color::from_rgb(0x33, 0x33, 0x33);
|
||||
static constexpr Color DIM_TEXT = Color::from_rgb(0x66, 0x66, 0x66);
|
||||
static constexpr Color ACCENT = Color::from_rgb(0x36, 0x7B, 0xF0);
|
||||
static constexpr Color ROW_HOVER = Color::from_rgb(0xF0, 0xF4, 0xFF);
|
||||
static constexpr Color BORDER = Color::from_rgb(0xCC, 0xCC, 0xCC);
|
||||
static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
static constexpr Color GREEN = Color::from_rgb(0x2E, 0xA0, 0x43);
|
||||
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
|
||||
// ============================================================================
|
||||
|
||||
enum Tab { TAB_DEVICES = 0, TAB_SCAN = 1 };
|
||||
|
||||
static TrueTypeFont* g_font = nullptr;
|
||||
static int g_win_w = WIN_W;
|
||||
static int g_win_h = WIN_H;
|
||||
|
||||
static Tab g_tab = TAB_DEVICES;
|
||||
static int g_hover_row = -1;
|
||||
|
||||
// Adapter info
|
||||
static Montauk::BtAdapterInfo g_adapter;
|
||||
static bool g_adapter_ok = false;
|
||||
|
||||
// Connected/paired devices
|
||||
static Montauk::BtDevInfo g_devices[MAX_CONNECTED];
|
||||
static char g_device_names[MAX_CONNECTED][64];
|
||||
static int g_device_count = 0;
|
||||
|
||||
// Scan results
|
||||
static Montauk::BtScanResult g_scan[MAX_SCAN];
|
||||
static int g_scan_count = 0;
|
||||
static bool g_scanning = false;
|
||||
|
||||
// Scroll
|
||||
static int g_scroll = 0;
|
||||
|
||||
// Status message
|
||||
static char g_status[80];
|
||||
static uint64_t g_status_time = 0;
|
||||
|
||||
// ============================================================================
|
||||
// Pixel helpers
|
||||
// ============================================================================
|
||||
|
||||
static void px_fill(uint32_t* px, int bw, int bh,
|
||||
int x, int y, int w, int h, Color c) {
|
||||
uint32_t v = c.to_pixel();
|
||||
int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y;
|
||||
int x1 = x + w > bw ? bw : x + w;
|
||||
int y1 = y + h > bh ? bh : y + h;
|
||||
for (int row = y0; row < y1; row++)
|
||||
for (int col = x0; col < x1; col++)
|
||||
px[row * bw + col] = v;
|
||||
}
|
||||
|
||||
static void px_fill_rounded(uint32_t* px, int bw, int bh,
|
||||
int x, int y, int w, int h, int r, Color c) {
|
||||
uint32_t v = c.to_pixel();
|
||||
for (int row = 0; row < h; row++) {
|
||||
int dy = y + row;
|
||||
if (dy < 0 || dy >= bh) continue;
|
||||
for (int col = 0; col < w; col++) {
|
||||
int dx = x + col;
|
||||
if (dx < 0 || dx >= bw) continue;
|
||||
bool skip = false;
|
||||
int cx2, cy2;
|
||||
if (col < r && row < r) { cx2 = r - col - 1; cy2 = r - row - 1; if (cx2*cx2 + cy2*cy2 >= r*r) skip = true; }
|
||||
else if (col >= w - r && row < r) { cx2 = col - (w - r); cy2 = r - row - 1; if (cx2*cx2 + cy2*cy2 >= r*r) skip = true; }
|
||||
else if (col < r && row >= h - r) { cx2 = r - col - 1; cy2 = row - (h - r); if (cx2*cx2 + cy2*cy2 >= r*r) skip = true; }
|
||||
else if (col >= w - r && row >= h - r) { cx2 = col - (w - r); cy2 = row - (h - r); if (cx2*cx2 + cy2*cy2 >= r*r) skip = true; }
|
||||
if (!skip) px[dy * bw + dx] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void px_hline(uint32_t* px, int bw, int bh, int x, int y, int w, Color c) {
|
||||
if (y < 0 || y >= bh) return;
|
||||
uint32_t v = c.to_pixel();
|
||||
int x0 = x < 0 ? 0 : x;
|
||||
int x1 = x + w > bw ? bw : x + w;
|
||||
for (int col = x0; col < x1; col++)
|
||||
px[y * bw + col] = v;
|
||||
}
|
||||
|
||||
static void px_circle(uint32_t* px, int bw, int bh,
|
||||
int cx, int cy, int r, Color c) {
|
||||
uint32_t v = c.to_pixel();
|
||||
for (int dy = -r; dy <= r; dy++) {
|
||||
int py = cy + dy;
|
||||
if (py < 0 || py >= bh) continue;
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
int ppx = cx + dx;
|
||||
if (ppx < 0 || ppx >= bw) continue;
|
||||
if (dx * dx + dy * dy <= r * r)
|
||||
px[py * bw + ppx] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void px_text(uint32_t* px, int bw, int bh,
|
||||
int x, int y, const char* text, Color c, int size = FONT_SIZE) {
|
||||
if (g_font)
|
||||
g_font->draw_to_buffer(px, bw, bh, x, y, text, c, size);
|
||||
}
|
||||
|
||||
static int text_w(const char* text, int size = FONT_SIZE) {
|
||||
return g_font ? g_font->measure_text(text, size) : 0;
|
||||
}
|
||||
|
||||
static int font_h(int size = FONT_SIZE) {
|
||||
if (!g_font) return 14;
|
||||
auto* cache = g_font->get_cache(size);
|
||||
return cache->ascent - cache->descent;
|
||||
}
|
||||
|
||||
static void px_button(uint32_t* px, int bw, int bh,
|
||||
int x, int y, int w, int h,
|
||||
const char* label, Color bg, Color fg, int r = BTN_RAD) {
|
||||
px_fill_rounded(px, bw, bh, x, y, w, h, r, bg);
|
||||
int tw = text_w(label, FONT_SIZE_SM);
|
||||
int fh = font_h(FONT_SIZE_SM);
|
||||
px_text(px, bw, bh, x + (w - tw) / 2, y + (h - fh) / 2 - 1, label, fg, FONT_SIZE_SM);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Bluetooth helpers
|
||||
// ============================================================================
|
||||
|
||||
static void set_status(const char* msg) {
|
||||
snprintf(g_status, sizeof(g_status), "%s", msg);
|
||||
g_status_time = montauk::get_milliseconds();
|
||||
}
|
||||
|
||||
static void refresh_adapter() {
|
||||
montauk::memset(&g_adapter, 0, sizeof(g_adapter));
|
||||
int r = montauk::bt_info(&g_adapter);
|
||||
g_adapter_ok = (r >= 0 && g_adapter.initialized);
|
||||
}
|
||||
|
||||
static void refresh_devices() {
|
||||
g_device_count = montauk::bt_list(g_devices, MAX_CONNECTED);
|
||||
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++) {
|
||||
bool found = false;
|
||||
for (int j = 0; j < g_scan_count; j++) {
|
||||
if (memcmp(g_devices[i].bdAddr, g_scan[j].bdAddr, 6) == 0 && g_scan[j].name[0]) {
|
||||
montauk::memcpy(g_device_names[i], g_scan[j].name, 64);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
snprintf(g_device_names[i], 64, "%02X:%02X:%02X:%02X:%02X:%02X",
|
||||
g_devices[i].bdAddr[0], g_devices[i].bdAddr[1],
|
||||
g_devices[i].bdAddr[2], g_devices[i].bdAddr[3],
|
||||
g_devices[i].bdAddr[4], g_devices[i].bdAddr[5]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void do_scan() {
|
||||
g_scanning = true;
|
||||
g_scan_count = 0;
|
||||
set_status("Scanning...");
|
||||
}
|
||||
|
||||
static void finish_scan() {
|
||||
int r = montauk::bt_scan(g_scan, MAX_SCAN, 3000);
|
||||
g_scan_count = r > 0 ? r : 0;
|
||||
g_scanning = false;
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof(msg), "Found %d device%s", g_scan_count, g_scan_count == 1 ? "" : "s");
|
||||
set_status(msg);
|
||||
}
|
||||
|
||||
static void do_connect(const uint8_t* addr) {
|
||||
int r = montauk::bt_connect(addr);
|
||||
if (r >= 0) {
|
||||
set_status("Connected");
|
||||
} else {
|
||||
set_status("Connection failed");
|
||||
}
|
||||
refresh_devices();
|
||||
}
|
||||
|
||||
static void do_disconnect(const uint8_t* addr) {
|
||||
int r = montauk::bt_disconnect(addr);
|
||||
if (r >= 0) {
|
||||
set_status("Disconnected");
|
||||
} else {
|
||||
set_status("Disconnect failed");
|
||||
}
|
||||
refresh_devices();
|
||||
}
|
||||
|
||||
static bool is_connected(const uint8_t* addr) {
|
||||
for (int i = 0; i < g_device_count; i++) {
|
||||
if (memcmp(g_devices[i].bdAddr, addr, 6) == 0 && g_devices[i].connected)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Format helpers
|
||||
// ============================================================================
|
||||
|
||||
static const char* device_class_str(uint32_t cod) {
|
||||
uint32_t major = (cod >> 8) & 0x1F;
|
||||
switch (major) {
|
||||
case 1: return "Computer";
|
||||
case 2: return "Phone";
|
||||
case 3: return "Network";
|
||||
case 4: return "Audio/Video";
|
||||
case 5: return "Peripheral";
|
||||
case 6: return "Imaging";
|
||||
case 7: return "Wearable";
|
||||
default: return "Device";
|
||||
}
|
||||
}
|
||||
|
||||
static void format_addr(char* buf, int len, const uint8_t* addr) {
|
||||
snprintf(buf, len, "%02X:%02X:%02X:%02X:%02X:%02X",
|
||||
addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
|
||||
}
|
||||
|
||||
static const char* rssi_bar(int8_t rssi) {
|
||||
if (rssi >= -50) return "Strong";
|
||||
if (rssi >= -70) return "Good";
|
||||
if (rssi >= -85) return "Weak";
|
||||
return "Very weak";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Render
|
||||
// ============================================================================
|
||||
|
||||
static void render(uint32_t* pixels) {
|
||||
int W = g_win_w;
|
||||
int H = g_win_h;
|
||||
|
||||
// Background
|
||||
px_fill(pixels, W, H, 0, 0, W, H, BG_COLOR);
|
||||
|
||||
// Tab bar
|
||||
int tab_y = 0;
|
||||
px_fill(pixels, W, H, 0, tab_y, W, TAB_H, TAB_BG);
|
||||
px_hline(pixels, W, H, 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)
|
||||
px_fill(pixels, W, H, i * tab_w, tab_y, tab_w, TAB_H, BG_COLOR);
|
||||
|
||||
Color tc = (g_tab == i) ? TAB_ACTIVE : TAB_INACTIVE;
|
||||
int tw = text_w(tab_labels[i], FONT_SIZE);
|
||||
int tx = i * tab_w + (tab_w - tw) / 2;
|
||||
px_text(pixels, W, H, tx, tab_y + (TAB_H - font_h(FONT_SIZE)) / 2,
|
||||
tab_labels[i], tc, FONT_SIZE);
|
||||
// Active indicator underline
|
||||
if (g_tab == i) {
|
||||
px_fill(pixels, W, H, 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) {
|
||||
const char* msg = "No connected devices";
|
||||
int mw = text_w(msg, FONT_SIZE);
|
||||
px_text(pixels, W, H, (W - mw) / 2, content_y + list_h / 2 - font_h(FONT_SIZE) / 2,
|
||||
msg, DIM_TEXT, FONT_SIZE);
|
||||
} else {
|
||||
for (int i = 0; i < g_device_count; i++) {
|
||||
int ry = content_y + i * ROW_H - g_scroll;
|
||||
if (ry + ROW_H < content_y || ry >= content_y + list_h) continue;
|
||||
|
||||
// Hover highlight
|
||||
if (g_hover_row == i)
|
||||
px_fill(pixels, W, H, 0, ry, W, ROW_H, ROW_HOVER);
|
||||
|
||||
// Connected indicator dot
|
||||
Color dot_c = g_devices[i].connected ? GREEN : DIM_TEXT;
|
||||
px_circle(pixels, W, H, PAD + 6, ry + ROW_H / 2, 5, dot_c);
|
||||
|
||||
// Device name
|
||||
px_text(pixels, W, H, PAD + 20, ry + 8,
|
||||
g_device_names[i], TEXT_COLOR, FONT_SIZE);
|
||||
|
||||
// Address below name
|
||||
char addr_str[24];
|
||||
format_addr(addr_str, sizeof(addr_str), g_devices[i].bdAddr);
|
||||
px_text(pixels, W, H, PAD + 20, ry + 8 + font_h(FONT_SIZE) + 2,
|
||||
addr_str, DIM_TEXT, FONT_SIZE_SM);
|
||||
|
||||
// Disconnect button
|
||||
if (g_devices[i].connected) {
|
||||
int bx = W - PAD - BTN_W;
|
||||
int by = ry + (ROW_H - BTN_H) / 2;
|
||||
px_button(pixels, W, H, bx, by, BTN_W, BTN_H,
|
||||
"Disconnect", RED, WHITE);
|
||||
}
|
||||
|
||||
// Divider
|
||||
px_hline(pixels, W, H, PAD, ry + ROW_H - 1, W - 2 * PAD, BORDER);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Scan tab
|
||||
// Scan button at top of content area
|
||||
int scan_btn_y = content_y + 12;
|
||||
int scan_btn_w = 100;
|
||||
int scan_btn_x = (W - scan_btn_w) / 2;
|
||||
if (g_scanning) {
|
||||
px_button(pixels, W, H, scan_btn_x, scan_btn_y, scan_btn_w, BTN_H,
|
||||
"Scanning...", BORDER, DIM_TEXT);
|
||||
} else {
|
||||
px_button(pixels, W, H, scan_btn_x, scan_btn_y, scan_btn_w, BTN_H,
|
||||
"Scan", ACCENT, WHITE);
|
||||
}
|
||||
|
||||
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) {
|
||||
const char* msg = "Press Scan to find devices";
|
||||
int mw = text_w(msg, FONT_SIZE);
|
||||
px_text(pixels, W, H, (W - mw) / 2, list_top + scan_list_h / 2 - font_h(FONT_SIZE) / 2,
|
||||
msg, DIM_TEXT, FONT_SIZE);
|
||||
} else {
|
||||
for (int i = 0; i < g_scan_count; i++) {
|
||||
int ry = list_top + i * ROW_H - g_scroll;
|
||||
if (ry + ROW_H < list_top || ry >= list_top + scan_list_h) continue;
|
||||
|
||||
// Hover highlight
|
||||
if (g_hover_row == i)
|
||||
px_fill(pixels, W, H, 0, ry, W, ROW_H, ROW_HOVER);
|
||||
|
||||
// Device type indicator
|
||||
const char* type_str = device_class_str(g_scan[i].classOfDevice);
|
||||
px_circle(pixels, W, H, PAD + 6, ry + ROW_H / 2, 5, ACCENT);
|
||||
|
||||
// Device name (or address if unnamed)
|
||||
const char* display_name = g_scan[i].name[0] ? g_scan[i].name : "Unknown Device";
|
||||
px_text(pixels, W, H, PAD + 20, ry + 4,
|
||||
display_name, TEXT_COLOR, FONT_SIZE);
|
||||
|
||||
// Type + signal below name
|
||||
char detail[80];
|
||||
char addr_str[24];
|
||||
format_addr(addr_str, sizeof(addr_str), g_scan[i].bdAddr);
|
||||
snprintf(detail, sizeof(detail), "%s | %s | %s",
|
||||
type_str, addr_str, rssi_bar(g_scan[i].rssi));
|
||||
px_text(pixels, W, H, PAD + 20, ry + 4 + font_h(FONT_SIZE) + 2,
|
||||
detail, DIM_TEXT, FONT_SIZE_SM);
|
||||
|
||||
// Connect/Disconnect button
|
||||
bool conn = is_connected(g_scan[i].bdAddr);
|
||||
int bx = W - PAD - BTN_W;
|
||||
int by = ry + (ROW_H - BTN_H) / 2;
|
||||
if (conn) {
|
||||
px_button(pixels, W, H, bx, by, BTN_W, BTN_H,
|
||||
"Disconnect", RED, WHITE);
|
||||
} else {
|
||||
px_button(pixels, W, H, bx, by, BTN_W, BTN_H,
|
||||
"Connect", ACCENT, WHITE);
|
||||
}
|
||||
|
||||
// Divider
|
||||
px_hline(pixels, W, H, PAD, ry + ROW_H - 1, W - 2 * PAD, BORDER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status bar
|
||||
int status_y = H - STATUS_H;
|
||||
px_fill(pixels, W, H, 0, status_y, W, STATUS_H, TAB_BG);
|
||||
px_hline(pixels, W, H, 0, status_y, W, BORDER);
|
||||
|
||||
// Status bar content: adapter info on the left, status message on the right
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
int sy = status_y + (STATUS_H - font_h(FONT_SIZE_SM)) / 2;
|
||||
|
||||
if (g_adapter_ok) {
|
||||
char adapter_info[96];
|
||||
char addr_str[24];
|
||||
format_addr(addr_str, sizeof(addr_str), g_adapter.bdAddr);
|
||||
const char* name = g_adapter.name[0] ? g_adapter.name : "Adapter";
|
||||
snprintf(adapter_info, sizeof(adapter_info), "%s | %s | %d connected",
|
||||
name, addr_str, g_device_count);
|
||||
px_text(pixels, W, H, PAD, sy, adapter_info, DIM_TEXT, FONT_SIZE_SM);
|
||||
} else {
|
||||
px_text(pixels, W, H, PAD, sy, "No adapter found", RED, FONT_SIZE_SM);
|
||||
}
|
||||
|
||||
// Temporary status message (right-aligned, fades after 5s)
|
||||
if (g_status[0] && (now - g_status_time) < 5000) {
|
||||
int sw = text_w(g_status, FONT_SIZE_SM);
|
||||
px_text(pixels, W, H, W - PAD - sw, sy, g_status, DIM_TEXT, FONT_SIZE_SM);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hit testing
|
||||
// ============================================================================
|
||||
|
||||
static bool handle_click(int mx, int my) {
|
||||
int W = g_win_w;
|
||||
|
||||
// Tab bar
|
||||
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;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Entry point
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void _start() {
|
||||
// Load font
|
||||
{
|
||||
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_devices();
|
||||
g_status[0] = 0;
|
||||
|
||||
// Create window
|
||||
Montauk::WinCreateResult wres;
|
||||
if (montauk::win_create("Bluetooth", WIN_W, WIN_H, &wres) < 0 || wres.id < 0)
|
||||
montauk::exit(1);
|
||||
|
||||
int win_id = wres.id;
|
||||
uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
|
||||
|
||||
render(pixels);
|
||||
montauk::win_present(win_id);
|
||||
|
||||
// Periodic refresh timer
|
||||
uint64_t last_refresh = montauk::get_milliseconds();
|
||||
|
||||
while (true) {
|
||||
Montauk::WinEvent ev;
|
||||
int r = montauk::win_poll(win_id, &ev);
|
||||
|
||||
if (r < 0) break;
|
||||
|
||||
// Handle pending scan (non-blocking: kick it off after render)
|
||||
if (g_scanning) {
|
||||
render(pixels);
|
||||
montauk::win_present(win_id);
|
||||
finish_scan();
|
||||
refresh_devices();
|
||||
render(pixels);
|
||||
montauk::win_present(win_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (r == 0) {
|
||||
// Periodic refresh every 5 seconds
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (now - last_refresh >= 5000) {
|
||||
refresh_adapter();
|
||||
refresh_devices();
|
||||
last_refresh = now;
|
||||
render(pixels);
|
||||
montauk::win_present(win_id);
|
||||
}
|
||||
montauk::sleep_ms(16);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool redraw = false;
|
||||
|
||||
if (ev.type == 3) break; // close
|
||||
|
||||
// Resize
|
||||
if (ev.type == 2) {
|
||||
g_win_w = ev.resize.w;
|
||||
g_win_h = ev.resize.h;
|
||||
pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g_win_w, g_win_h);
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
// Keyboard
|
||||
if (ev.type == 0 && ev.key.pressed) {
|
||||
if (ev.key.scancode == 0x01) break; // Escape
|
||||
if (ev.key.ascii == 's' || ev.key.ascii == 'S') {
|
||||
if (!g_scanning) { do_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) {
|
||||
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
|
||||
int new_hover = get_hover_row(ev.mouse.x, ev.mouse.y);
|
||||
if (new_hover != g_hover_row) {
|
||||
g_hover_row = new_hover;
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
if (clicked) {
|
||||
if (handle_click(ev.mouse.x, ev.mouse.y))
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
// Scroll
|
||||
if (ev.mouse.scroll) {
|
||||
g_scroll -= ev.mouse.scroll * 20;
|
||||
if (g_scroll < 0) g_scroll = 0;
|
||||
redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (redraw) {
|
||||
render(pixels);
|
||||
montauk::win_present(win_id);
|
||||
}
|
||||
}
|
||||
|
||||
montauk::win_destroy(win_id);
|
||||
montauk::exit(0);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[app]
|
||||
name = "Bluetooth"
|
||||
binary = "bluetooth.elf"
|
||||
icon = "bluetooth.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
visible = true
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* stb_truetype_impl.cpp
|
||||
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
// Override all stb_truetype dependencies before including the implementation
|
||||
|
||||
#define STBTT_ifloor(x) ((int) stb_floor(x))
|
||||
#define STBTT_iceil(x) ((int) stb_ceil(x))
|
||||
#define STBTT_sqrt(x) stb_sqrt(x)
|
||||
#define STBTT_pow(x,y) stb_pow(x,y)
|
||||
#define STBTT_fmod(x,y) stb_fmod(x,y)
|
||||
#define STBTT_cos(x) stb_cos(x)
|
||||
#define STBTT_acos(x) stb_acos(x)
|
||||
#define STBTT_fabs(x) stb_fabs(x)
|
||||
|
||||
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
|
||||
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
|
||||
|
||||
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
|
||||
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
|
||||
|
||||
#define STBTT_strlen(x) montauk::slen(x)
|
||||
|
||||
#define STBTT_assert(x) ((void)(x))
|
||||
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
@@ -77,6 +77,14 @@ static bool is_spreadsheet_file(const char* name) {
|
||||
return str_ends_with(name, ".mss");
|
||||
}
|
||||
|
||||
static bool is_video_file(const char* name) {
|
||||
return str_ends_with(name, ".mp4") || str_ends_with(name, ".m4v");
|
||||
}
|
||||
|
||||
static bool is_audio_file(const char* name) {
|
||||
return str_ends_with(name, ".mp3") || str_ends_with(name, ".wav");
|
||||
}
|
||||
|
||||
static int detect_file_type(const char* name, bool is_dir) {
|
||||
if (is_dir) return 1;
|
||||
if (str_ends_with(name, ".elf")) return 2;
|
||||
@@ -174,11 +182,7 @@ static void filemanager_read_dir(FileManagerState* fm) {
|
||||
fm->is_dir[i] = true;
|
||||
fm->entry_names[i][len - 1] = '\0';
|
||||
} else {
|
||||
bool has_dot = false;
|
||||
for (int j = 0; j < len; j++) {
|
||||
if (fm->entry_names[i][j] == '.') { has_dot = true; break; }
|
||||
}
|
||||
fm->is_dir[i] = !has_dot;
|
||||
fm->is_dir[i] = false;
|
||||
}
|
||||
|
||||
fm->entry_types[i] = detect_file_type(fm->entry_names[i], fm->is_dir[i]);
|
||||
@@ -240,6 +244,66 @@ static void filemanager_read_dir(FileManagerState* fm) {
|
||||
fm->last_click_time = 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Recursive directory deletion
|
||||
// ============================================================================
|
||||
|
||||
static bool filemanager_delete_recursive(const char* path) {
|
||||
// Try deleting as a file (or empty directory) first
|
||||
if (montauk::fdelete(path) == 0) return true;
|
||||
|
||||
// If that failed, it may be a non-empty directory — enumerate and delete children
|
||||
const char* names[64];
|
||||
int count = montauk::readdir(path, names, 64);
|
||||
if (count < 0) return false;
|
||||
|
||||
// Compute the prefix to strip (readdir returns paths relative to drive root)
|
||||
const char* after_drive = path;
|
||||
for (int k = 0; after_drive[k]; k++) {
|
||||
if (after_drive[k] == ':' && after_drive[k + 1] == '/') {
|
||||
after_drive += k + 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
char prefix[256] = {0};
|
||||
int prefix_len = 0;
|
||||
if (after_drive[0] != '\0') {
|
||||
montauk::strcpy(prefix, after_drive);
|
||||
prefix_len = montauk::slen(prefix);
|
||||
if (prefix_len > 0 && prefix[prefix_len - 1] != '/') {
|
||||
prefix[prefix_len++] = '/';
|
||||
prefix[prefix_len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char* raw = names[i];
|
||||
if (prefix_len > 0) {
|
||||
bool match = true;
|
||||
for (int k = 0; k < prefix_len; k++) {
|
||||
if (raw[k] != prefix[k]) { match = false; break; }
|
||||
}
|
||||
if (match) raw += prefix_len;
|
||||
}
|
||||
|
||||
char child[512];
|
||||
montauk::strcpy(child, path);
|
||||
int plen = montauk::slen(child);
|
||||
if (plen > 0 && child[plen - 1] != '/')
|
||||
str_append(child, "/", 512);
|
||||
str_append(child, raw, 512);
|
||||
|
||||
// Strip trailing slash if present (directory marker)
|
||||
int clen = montauk::slen(child);
|
||||
if (clen > 0 && child[clen - 1] == '/') child[clen - 1] = '\0';
|
||||
|
||||
filemanager_delete_recursive(child);
|
||||
}
|
||||
|
||||
// Now the directory should be empty — delete it
|
||||
return montauk::fdelete(path) == 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// History management
|
||||
// ============================================================================
|
||||
@@ -373,11 +437,12 @@ static void filemanager_draw_header(Canvas& c, FileManagerState* fm,
|
||||
}
|
||||
}
|
||||
|
||||
// Delete button (6th toolbar button) — only active when a file is selected
|
||||
// Delete button (6th toolbar button) — active when a file or directory is selected
|
||||
{
|
||||
int bx = 148, by = 4;
|
||||
bool has_sel = fm->selected >= 0 && fm->selected < fm->entry_count
|
||||
&& !fm->at_drives_root && !fm->is_dir[fm->selected];
|
||||
&& !fm->at_drives_root
|
||||
&& fm->entry_types[fm->selected] != 3;
|
||||
Color del_bg = has_sel ? btn_bg : Color::from_rgb(0xF0, 0xF0, 0xF0);
|
||||
c.fill_rect(bx, by, 24, 24, del_bg);
|
||||
if (ds && ds->icon_delete.pixels) {
|
||||
@@ -634,16 +699,19 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
fm->scrollbar.scroll_offset = 0;
|
||||
}
|
||||
else if (local_x >= 148 && local_x < 172) {
|
||||
// Delete selected file
|
||||
// Delete selected file or directory
|
||||
if (fm->selected >= 0 && fm->selected < fm->entry_count
|
||||
&& !fm->at_drives_root && !fm->is_dir[fm->selected]) {
|
||||
&& !fm->at_drives_root && fm->entry_types[fm->selected] != 3) {
|
||||
char fullpath[512];
|
||||
montauk::strcpy(fullpath, fm->current_path);
|
||||
int plen = montauk::slen(fullpath);
|
||||
if (plen > 0 && fullpath[plen - 1] != '/')
|
||||
str_append(fullpath, "/", 512);
|
||||
str_append(fullpath, fm->entry_names[fm->selected], 512);
|
||||
montauk::fdelete(fullpath);
|
||||
if (fm->is_dir[fm->selected])
|
||||
filemanager_delete_recursive(fullpath);
|
||||
else
|
||||
montauk::fdelete(fullpath);
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
}
|
||||
@@ -695,6 +763,10 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
montauk::spawn("0:/apps/pdfviewer/pdfviewer.elf", fullpath);
|
||||
} else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/apps/spreadsheet/spreadsheet.elf", fullpath);
|
||||
} else if (is_video_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/apps/video/video.elf", fullpath);
|
||||
} else if (is_audio_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/apps/music/music.elf", fullpath);
|
||||
} else if (str_ends_with(fm->entry_names[clicked_idx], ".elf")) {
|
||||
montauk::spawn(fullpath);
|
||||
} else if (fm->desktop) {
|
||||
@@ -753,6 +825,10 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
montauk::spawn("0:/apps/pdfviewer/pdfviewer.elf", fullpath);
|
||||
} else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/apps/spreadsheet/spreadsheet.elf", fullpath);
|
||||
} else if (is_video_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/apps/video/video.elf", fullpath);
|
||||
} else if (is_audio_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/apps/music/music.elf", fullpath);
|
||||
} else if (str_ends_with(fm->entry_names[clicked_idx], ".elf")) {
|
||||
montauk::spawn(fullpath);
|
||||
} else if (fm->desktop) {
|
||||
@@ -842,14 +918,17 @@ static void filemanager_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
} else if (key.scancode == 0x53) {
|
||||
// Delete key
|
||||
if (fm->selected >= 0 && fm->selected < fm->entry_count
|
||||
&& !fm->at_drives_root && !fm->is_dir[fm->selected]) {
|
||||
&& !fm->at_drives_root && fm->entry_types[fm->selected] != 3) {
|
||||
char fullpath[512];
|
||||
montauk::strcpy(fullpath, fm->current_path);
|
||||
int plen = montauk::slen(fullpath);
|
||||
if (plen > 0 && fullpath[plen - 1] != '/')
|
||||
str_append(fullpath, "/", 512);
|
||||
str_append(fullpath, fm->entry_names[fm->selected], 512);
|
||||
montauk::fdelete(fullpath);
|
||||
if (fm->is_dir[fm->selected])
|
||||
filemanager_delete_recursive(fullpath);
|
||||
else
|
||||
montauk::fdelete(fullpath);
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
} else if (key.alt && key.scancode == 0x4B) {
|
||||
|
||||
@@ -262,6 +262,8 @@ void gui::desktop_init(DesktopState* ds) {
|
||||
ds->net_cfg_last_poll = montauk::get_milliseconds();
|
||||
ds->net_icon_rect = {0, 0, 0, 0};
|
||||
|
||||
ds->closing_ext_count = 0;
|
||||
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -302,6 +304,16 @@ void desktop_poll_external_windows(DesktopState* ds) {
|
||||
}
|
||||
|
||||
if (!found && ds->window_count < MAX_WINDOWS) {
|
||||
// Skip windows we've already sent a close event to — the owning
|
||||
// process hasn't destroyed them yet, so they still appear in
|
||||
// win_enumerate. Re-creating them would place them at the
|
||||
// default position instead of where the user dragged them.
|
||||
bool closing = false;
|
||||
for (int c = 0; c < ds->closing_ext_count; c++) {
|
||||
if (ds->closing_ext_ids[c] == extId) { closing = true; break; }
|
||||
}
|
||||
if (closing) continue;
|
||||
|
||||
// Map the pixel buffer into our address space
|
||||
uint64_t va = montauk::win_map(extId);
|
||||
if (va == 0) continue;
|
||||
@@ -367,6 +379,17 @@ void desktop_poll_external_windows(DesktopState* ds) {
|
||||
gui::desktop_close_window(ds, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Purge closing IDs that the kernel no longer reports (fully destroyed)
|
||||
for (int c = ds->closing_ext_count - 1; c >= 0; c--) {
|
||||
bool alive = false;
|
||||
for (int e = 0; e < extCount; e++) {
|
||||
if (extWins[e].id == ds->closing_ext_ids[c]) { alive = true; break; }
|
||||
}
|
||||
if (!alive) {
|
||||
ds->closing_ext_ids[c] = ds->closing_ext_ids[--ds->closing_ext_count];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -55,8 +55,13 @@ void gui::desktop_close_window(DesktopState* ds, int idx) {
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
|
||||
// For external windows, send a close event instead of freeing the buffer
|
||||
// For external windows, send a close event instead of freeing the buffer.
|
||||
// Track the ID so the poll loop won't re-create it at the default position
|
||||
// before the owning process has finished destroying it.
|
||||
if (win->external) {
|
||||
if (ds->closing_ext_count < DesktopState::MAX_CLOSING) {
|
||||
ds->closing_ext_ids[ds->closing_ext_count++] = win->ext_win_id;
|
||||
}
|
||||
Montauk::WinEvent ev;
|
||||
montauk::memset(&ev, 0, sizeof(ev));
|
||||
ev.type = 3; // close
|
||||
|
||||
@@ -29,20 +29,70 @@ void disktool_refresh() {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Create partition
|
||||
// Create partition — confirmation dialog
|
||||
// ============================================================================
|
||||
|
||||
void do_create_partition() {
|
||||
void open_newpart_dialog() {
|
||||
auto& dt = g_state;
|
||||
if (dt.selected_disk < 0 || dt.selected_disk >= dt.disk_count) return;
|
||||
if (dt.np_dlg.open) return;
|
||||
|
||||
int part_indices[MAX_PARTS];
|
||||
int nparts = get_disk_parts(part_indices, MAX_PARTS);
|
||||
|
||||
if (nparts == 0) {
|
||||
auto& dlg = dt.np_dlg;
|
||||
dlg.will_init_gpt = (nparts == 0);
|
||||
dlg.hover_confirm = false;
|
||||
dlg.hover_cancel = false;
|
||||
|
||||
Montauk::DiskInfo& disk = dt.disks[dt.selected_disk];
|
||||
char sz[24];
|
||||
format_disk_size(sz, sizeof(sz), disk.sectorCount, disk.sectorSizeLog);
|
||||
snprintf(dlg.disk_desc, sizeof(dlg.disk_desc), "Disk %d: %s (%s)",
|
||||
dt.selected_disk, disk.model, sz);
|
||||
|
||||
if (dlg.will_init_gpt) {
|
||||
snprintf(dlg.warn_line1, sizeof(dlg.warn_line1),
|
||||
"This will initialize a new GPT on this disk.");
|
||||
snprintf(dlg.warn_line2, sizeof(dlg.warn_line2),
|
||||
"ALL existing partitions will be destroyed!");
|
||||
} else {
|
||||
snprintf(dlg.warn_line1, sizeof(dlg.warn_line1),
|
||||
"This will add a new partition in the largest");
|
||||
snprintf(dlg.warn_line2, sizeof(dlg.warn_line2),
|
||||
"free region on this disk.");
|
||||
}
|
||||
|
||||
Montauk::WinCreateResult wres;
|
||||
const char* title = dlg.will_init_gpt ? "Initialize Disk" : "New Partition";
|
||||
if (montauk::win_create(title, NP_DLG_W, NP_DLG_H, &wres) < 0 || wres.id < 0) {
|
||||
set_status("Failed to open dialog");
|
||||
return;
|
||||
}
|
||||
dlg.win_id = wres.id;
|
||||
dlg.pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
|
||||
dlg.open = true;
|
||||
|
||||
render_newpart_window();
|
||||
montauk::win_present(dlg.win_id);
|
||||
}
|
||||
|
||||
void close_newpart_dialog() {
|
||||
auto& dlg = g_state.np_dlg;
|
||||
if (!dlg.open) return;
|
||||
montauk::win_destroy(dlg.win_id);
|
||||
dlg.open = false;
|
||||
}
|
||||
|
||||
void newpart_dialog_confirm() {
|
||||
auto& dt = g_state;
|
||||
auto& dlg = dt.np_dlg;
|
||||
|
||||
if (dlg.will_init_gpt) {
|
||||
int r = montauk::gpt_init(dt.selected_disk);
|
||||
if (r < 0) {
|
||||
set_status("Failed to initialize GPT on disk");
|
||||
close_newpart_dialog();
|
||||
return;
|
||||
}
|
||||
set_status("Initialized GPT");
|
||||
@@ -69,13 +119,18 @@ void do_create_partition() {
|
||||
int r = montauk::gpt_add(¶ms);
|
||||
if (r < 0) {
|
||||
set_status("Failed to create partition");
|
||||
return;
|
||||
} else {
|
||||
set_status("Partition created successfully");
|
||||
}
|
||||
|
||||
set_status("Partition created successfully");
|
||||
close_newpart_dialog();
|
||||
disktool_refresh();
|
||||
}
|
||||
|
||||
void do_create_partition() {
|
||||
open_newpart_dialog();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mount partition
|
||||
// ============================================================================
|
||||
|
||||
@@ -72,6 +72,8 @@ static constexpr int NUM_FS_TYPES = 1;
|
||||
|
||||
static constexpr int FMT_DLG_W = 280;
|
||||
static constexpr int FMT_DLG_H = 220;
|
||||
static constexpr int NP_DLG_W = 340;
|
||||
static constexpr int NP_DLG_H = 200;
|
||||
|
||||
struct FormatDialog {
|
||||
bool open;
|
||||
@@ -84,6 +86,18 @@ struct FormatDialog {
|
||||
uint32_t* pixels;
|
||||
};
|
||||
|
||||
struct NewPartDialog {
|
||||
bool open;
|
||||
bool will_init_gpt; // true if GPT init is needed (most destructive)
|
||||
bool hover_confirm;
|
||||
bool hover_cancel;
|
||||
char disk_desc[80];
|
||||
char warn_line1[96];
|
||||
char warn_line2[96];
|
||||
int win_id;
|
||||
uint32_t* pixels;
|
||||
};
|
||||
|
||||
struct DiskToolState {
|
||||
Montauk::DiskInfo disks[MAX_DISKS];
|
||||
int disk_count;
|
||||
@@ -95,6 +109,7 @@ struct DiskToolState {
|
||||
char status[80];
|
||||
uint64_t status_time;
|
||||
FormatDialog fmt_dlg;
|
||||
NewPartDialog np_dlg;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -125,6 +140,7 @@ void format_disk_size(char* buf, int bufsize, uint64_t sectors, uint16_t sectorS
|
||||
|
||||
void render(uint32_t* pixels);
|
||||
void render_format_window();
|
||||
void render_newpart_window();
|
||||
|
||||
// ============================================================================
|
||||
// Function declarations — actions.cpp
|
||||
@@ -136,3 +152,6 @@ void do_mount_partition();
|
||||
void open_format_dialog();
|
||||
void close_format_dialog();
|
||||
void format_dialog_do_format();
|
||||
void open_newpart_dialog();
|
||||
void close_newpart_dialog();
|
||||
void newpart_dialog_confirm();
|
||||
|
||||
@@ -180,6 +180,40 @@ static bool handle_content_click(int mx, int my) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// New partition dialog mouse handling
|
||||
// ============================================================================
|
||||
|
||||
static bool handle_newpart_dialog_click(int mx, int my, bool clicked) {
|
||||
auto& dlg = g_state.np_dlg;
|
||||
if (!dlg.open) return false;
|
||||
|
||||
int dw = NP_DLG_W, dh = NP_DLG_H;
|
||||
|
||||
int btn_w = 90, btn_h = 30;
|
||||
int btn_y = dh - btn_h - 16;
|
||||
int gap = 16;
|
||||
int total_w = btn_w * 2 + gap;
|
||||
int bx = (dw - total_w) / 2;
|
||||
|
||||
dlg.hover_confirm = (mx >= bx && mx < bx + btn_w && my >= btn_y && my < btn_y + btn_h);
|
||||
dlg.hover_cancel = (mx >= bx + btn_w + gap && mx < bx + btn_w * 2 + gap && my >= btn_y && my < btn_y + btn_h);
|
||||
|
||||
if (!clicked) return true;
|
||||
|
||||
if (dlg.hover_confirm) {
|
||||
newpart_dialog_confirm();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dlg.hover_cancel) {
|
||||
close_newpart_dialog();
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Format dialog mouse handling
|
||||
// ============================================================================
|
||||
@@ -236,6 +270,16 @@ static bool handle_key(const Montauk::KeyEvent& key) {
|
||||
|
||||
auto& dt = g_state;
|
||||
|
||||
// New partition dialog keys
|
||||
if (dt.np_dlg.open) {
|
||||
if (key.scancode == 0x01) { // Escape
|
||||
close_newpart_dialog();
|
||||
} else if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
newpart_dialog_confirm();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Format dialog keys
|
||||
if (dt.fmt_dlg.open) {
|
||||
if (key.scancode == 0x01) { // Escape
|
||||
@@ -313,6 +357,28 @@ extern "C" void _start() {
|
||||
bool redraw_main = false;
|
||||
bool redraw_dlg = false;
|
||||
|
||||
bool redraw_np = false;
|
||||
|
||||
// Poll new partition dialog window
|
||||
if (g_state.np_dlg.open) {
|
||||
int dr = montauk::win_poll(g_state.np_dlg.win_id, &ev);
|
||||
if (dr > 0) {
|
||||
if (ev.type == 3) { // close
|
||||
close_newpart_dialog();
|
||||
redraw_main = true;
|
||||
} else if (ev.type == 0 && ev.key.pressed) {
|
||||
handle_key(ev.key);
|
||||
redraw_np = g_state.np_dlg.open;
|
||||
redraw_main = true;
|
||||
} else if (ev.type == 1) {
|
||||
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
handle_newpart_dialog_click(ev.mouse.x, ev.mouse.y, clicked);
|
||||
redraw_np = g_state.np_dlg.open;
|
||||
redraw_main = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Poll format dialog window
|
||||
if (g_state.fmt_dlg.open) {
|
||||
int dr = montauk::win_poll(g_state.fmt_dlg.win_id, &ev);
|
||||
@@ -341,7 +407,8 @@ extern "C" void _start() {
|
||||
// Even with no main event, dialog may have triggered redraws
|
||||
if (redraw_main) { render(pixels); montauk::win_present(win_id); }
|
||||
if (redraw_dlg) { render_format_window(); montauk::win_present(g_state.fmt_dlg.win_id); }
|
||||
if (!redraw_main && !redraw_dlg) montauk::sleep_ms(16);
|
||||
if (redraw_np) { render_newpart_window(); montauk::win_present(g_state.np_dlg.win_id); }
|
||||
if (!redraw_main && !redraw_dlg && !redraw_np) montauk::sleep_ms(16);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -383,8 +450,10 @@ extern "C" void _start() {
|
||||
|
||||
if (redraw_main) { render(pixels); montauk::win_present(win_id); }
|
||||
if (redraw_dlg) { render_format_window(); montauk::win_present(g_state.fmt_dlg.win_id); }
|
||||
if (redraw_np) { render_newpart_window(); montauk::win_present(g_state.np_dlg.win_id); }
|
||||
}
|
||||
|
||||
close_newpart_dialog();
|
||||
close_format_dialog();
|
||||
montauk::win_destroy(win_id);
|
||||
montauk::exit(0);
|
||||
|
||||
@@ -345,6 +345,61 @@ void render_format_window() {
|
||||
px_button(px, dw, dh, bx + btn_w + gap, btn_y, btn_w, btn_h, "Cancel", can_bg, WHITE, 6);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Render: new partition confirmation dialog
|
||||
// ============================================================================
|
||||
|
||||
void render_newpart_window() {
|
||||
auto& dlg = g_state.np_dlg;
|
||||
if (!dlg.open) return;
|
||||
|
||||
uint32_t* px = dlg.pixels;
|
||||
int dw = NP_DLG_W, dh = NP_DLG_H;
|
||||
int fh = font_h();
|
||||
|
||||
px_fill(px, dw, dh, 0, 0, dw, dh, BG_COLOR);
|
||||
|
||||
// Title
|
||||
const char* title = dlg.will_init_gpt ? "Initialize Disk" : "New Partition";
|
||||
int tw = text_w(title);
|
||||
px_text(px, dw, dh, (dw - tw) / 2, 12, title, TEXT_COLOR);
|
||||
|
||||
// Disk description
|
||||
int ddw = text_w(dlg.disk_desc);
|
||||
px_text(px, dw, dh, (dw - ddw) / 2, 12 + fh + 6, dlg.disk_desc, DIM_TEXT);
|
||||
|
||||
// Warning lines
|
||||
Color warn_color = dlg.will_init_gpt
|
||||
? Color::from_rgb(0xCC, 0x22, 0x22)
|
||||
: TEXT_COLOR;
|
||||
|
||||
int wy = 12 + fh * 2 + 20;
|
||||
int w1w = text_w(dlg.warn_line1);
|
||||
px_text(px, dw, dh, (dw - w1w) / 2, wy, dlg.warn_line1, warn_color);
|
||||
int w2w = text_w(dlg.warn_line2);
|
||||
px_text(px, dw, dh, (dw - w2w) / 2, wy + fh + 4, dlg.warn_line2, warn_color);
|
||||
|
||||
// Buttons
|
||||
int btn_w = 90, btn_h = 30;
|
||||
int btn_y = dh - btn_h - 16;
|
||||
int gap = 16;
|
||||
int total_w = btn_w * 2 + gap;
|
||||
int bx = (dw - total_w) / 2;
|
||||
|
||||
Color confirm_bg = dlg.will_init_gpt
|
||||
? (dlg.hover_confirm ? Color::from_rgb(0xDD, 0x44, 0x44)
|
||||
: Color::from_rgb(0xCC, 0x33, 0x33))
|
||||
: (dlg.hover_confirm ? Color::from_rgb(0x4A, 0x88, 0xC8)
|
||||
: Color::from_rgb(0x42, 0x7A, 0xB5));
|
||||
const char* confirm_label = dlg.will_init_gpt ? "Initialize" : "Create";
|
||||
px_button(px, dw, dh, bx, btn_y, btn_w, btn_h, confirm_label, confirm_bg, WHITE, 6);
|
||||
|
||||
Color can_bg = dlg.hover_cancel
|
||||
? Color::from_rgb(0x99, 0x99, 0x99)
|
||||
: Color::from_rgb(0x88, 0x88, 0x88);
|
||||
px_button(px, dw, dh, bx + btn_w + gap, btn_y, btn_w, btn_h, "Cancel", can_bg, WHITE, 6);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Top-level render
|
||||
// ============================================================================
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# Makefile for music (standalone Music Player) on MontaukOS
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
# ---- Toolchain ----
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
else
|
||||
CXX := g++
|
||||
endif
|
||||
|
||||
# ---- Paths ----
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
LIBDIR := ../../lib
|
||||
|
||||
# ---- Compiler flags ----
|
||||
|
||||
CXXFLAGS := \
|
||||
-std=gnu++20 \
|
||||
-g -O2 -pipe \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-Wno-unused-function \
|
||||
-nostdinc \
|
||||
-ffreestanding \
|
||||
-fno-stack-protector \
|
||||
-fno-stack-check \
|
||||
-fno-PIC \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-msse \
|
||||
-msse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
-I $(PROG_INC) \
|
||||
-I . \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
# ---- Linker flags ----
|
||||
|
||||
LDFLAGS := \
|
||||
-nostdlib \
|
||||
-static \
|
||||
-Wl,--build-id=none \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,-m,elf_x86_64 \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
# ---- Source files ----
|
||||
|
||||
SRCS := main.cpp minimp3_impl.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := $(BINDIR)/apps/music/music.elf
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/apps/music
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
[app]
|
||||
name = "Music"
|
||||
binary = "music.elf"
|
||||
icon = "audio-player.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
visible = true
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* minimp3_impl.cpp
|
||||
* Single compilation unit for minimp3 in MontaukOS freestanding environment
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
extern "C" {
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
}
|
||||
|
||||
#define MINIMP3_IMPLEMENTATION
|
||||
#include "minimp3.h"
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* stb_truetype_impl.cpp
|
||||
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
// Override all stb_truetype dependencies before including the implementation
|
||||
|
||||
#define STBTT_ifloor(x) ((int) stb_floor(x))
|
||||
#define STBTT_iceil(x) ((int) stb_ceil(x))
|
||||
#define STBTT_sqrt(x) stb_sqrt(x)
|
||||
#define STBTT_pow(x,y) stb_pow(x,y)
|
||||
#define STBTT_fmod(x,y) stb_fmod(x,y)
|
||||
#define STBTT_cos(x) stb_cos(x)
|
||||
#define STBTT_acos(x) stb_acos(x)
|
||||
#define STBTT_fabs(x) stb_fabs(x)
|
||||
|
||||
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
|
||||
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
|
||||
|
||||
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
|
||||
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
|
||||
|
||||
#define STBTT_strlen(x) montauk::slen(x)
|
||||
|
||||
#define STBTT_assert(x) ((void)(x))
|
||||
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
@@ -0,0 +1,86 @@
|
||||
# Makefile for volume (standalone Volume Control) on MontaukOS
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
# ---- Toolchain ----
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
else
|
||||
CXX := g++
|
||||
endif
|
||||
|
||||
# ---- Paths ----
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
LIBDIR := ../../lib
|
||||
|
||||
# ---- Compiler flags ----
|
||||
|
||||
CXXFLAGS := \
|
||||
-std=gnu++20 \
|
||||
-g -O2 -pipe \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-Wno-unused-function \
|
||||
-nostdinc \
|
||||
-ffreestanding \
|
||||
-fno-stack-protector \
|
||||
-fno-stack-check \
|
||||
-fno-PIC \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-msse \
|
||||
-msse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
# ---- Linker flags ----
|
||||
|
||||
LDFLAGS := \
|
||||
-nostdlib \
|
||||
-static \
|
||||
-Wl,--build-id=none \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,-m,elf_x86_64 \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
# ---- Source files ----
|
||||
|
||||
SRCS := main.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := $(BINDIR)/apps/volume/volume.elf
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/apps/volume
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS Volume Control
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int WIN_W = 280;
|
||||
static constexpr int WIN_H = 164;
|
||||
static constexpr int FONT_SIZE = 16;
|
||||
static constexpr int FONT_SIZE_LG = 28;
|
||||
|
||||
static constexpr int SLIDER_X = 24;
|
||||
static constexpr int SLIDER_W = WIN_W - 48;
|
||||
static constexpr int SLIDER_Y = 78;
|
||||
static constexpr int SLIDER_H = 8;
|
||||
static constexpr int KNOB_R = 10;
|
||||
|
||||
static constexpr int BTN_W = 48;
|
||||
static constexpr int BTN_H = 28;
|
||||
static constexpr int BTN_Y = 118;
|
||||
static constexpr int BTN_RAD = 6;
|
||||
|
||||
static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
static constexpr Color TEXT_COLOR = Color::from_rgb(0x22, 0x22, 0x22);
|
||||
static constexpr Color DIM_TEXT = Color::from_rgb(0x66, 0x66, 0x66);
|
||||
static constexpr Color ACCENT = Color::from_rgb(0x36, 0x7B, 0xF0);
|
||||
static constexpr Color ACCENT_HOVER = Color::from_rgb(0x2A, 0x62, 0xC8);
|
||||
static constexpr Color TRACK_BG = Color::from_rgb(0xDD, 0xDD, 0xDD);
|
||||
static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
static constexpr Color MUTE_COLOR = Color::from_rgb(0xCC, 0x33, 0x33);
|
||||
|
||||
// ============================================================================
|
||||
// State
|
||||
// ============================================================================
|
||||
|
||||
static TrueTypeFont* g_font = nullptr;
|
||||
static int g_volume = 80;
|
||||
static bool g_muted = false;
|
||||
static int g_pre_mute_vol = 80;
|
||||
static bool g_dragging = false;
|
||||
|
||||
// ============================================================================
|
||||
// Pixel helpers (same pattern as disks app)
|
||||
// ============================================================================
|
||||
|
||||
static void px_fill(uint32_t* px, int bw, int bh,
|
||||
int x, int y, int w, int h, Color c) {
|
||||
uint32_t v = c.to_pixel();
|
||||
int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y;
|
||||
int x1 = x + w > bw ? bw : x + w;
|
||||
int y1 = y + h > bh ? bh : y + h;
|
||||
for (int row = y0; row < y1; row++)
|
||||
for (int col = x0; col < x1; col++)
|
||||
px[row * bw + col] = v;
|
||||
}
|
||||
|
||||
static void px_hline(uint32_t* px, int bw, int bh, int x, int y, int w, Color c) {
|
||||
if (y < 0 || y >= bh) return;
|
||||
uint32_t v = c.to_pixel();
|
||||
int x0 = x < 0 ? 0 : x;
|
||||
int x1 = x + w > bw ? bw : x + w;
|
||||
for (int col = x0; col < x1; col++)
|
||||
px[y * bw + col] = v;
|
||||
}
|
||||
|
||||
static void px_fill_rounded(uint32_t* px, int bw, int bh,
|
||||
int x, int y, int w, int h, int r, Color c) {
|
||||
uint32_t v = c.to_pixel();
|
||||
for (int row = 0; row < h; row++) {
|
||||
int dy = y + row;
|
||||
if (dy < 0 || dy >= bh) continue;
|
||||
for (int col = 0; col < w; col++) {
|
||||
int dx = x + col;
|
||||
if (dx < 0 || dx >= bw) continue;
|
||||
bool skip = false;
|
||||
int cx, cy;
|
||||
if (col < r && row < r) { cx = r - col - 1; cy = r - row - 1; if (cx*cx + cy*cy >= r*r) skip = true; }
|
||||
else if (col >= w - r && row < r) { cx = col - (w - r); cy = r - row - 1; if (cx*cx + cy*cy >= r*r) skip = true; }
|
||||
else if (col < r && row >= h - r) { cx = r - col - 1; cy = row - (h - r); if (cx*cx + cy*cy >= r*r) skip = true; }
|
||||
else if (col >= w - r && row >= h - r) { cx = col - (w - r); cy = row - (h - r); if (cx*cx + cy*cy >= r*r) skip = true; }
|
||||
if (!skip) px[dy * bw + dx] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void px_circle(uint32_t* px, int bw, int bh,
|
||||
int cx, int cy, int r, Color c) {
|
||||
uint32_t v = c.to_pixel();
|
||||
for (int dy = -r; dy <= r; dy++) {
|
||||
int py = cy + dy;
|
||||
if (py < 0 || py >= bh) continue;
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
int ppx = cx + dx;
|
||||
if (ppx < 0 || ppx >= bw) continue;
|
||||
if (dx * dx + dy * dy <= r * r)
|
||||
px[py * bw + ppx] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void px_text(uint32_t* px, int bw, int bh,
|
||||
int x, int y, const char* text, Color c, int size = FONT_SIZE) {
|
||||
if (g_font)
|
||||
g_font->draw_to_buffer(px, bw, bh, x, y, text, c, size);
|
||||
}
|
||||
|
||||
static int text_w(const char* text, int size = FONT_SIZE) {
|
||||
return g_font ? g_font->measure_text(text, size) : 0;
|
||||
}
|
||||
|
||||
static int font_h(int size = FONT_SIZE) {
|
||||
if (!g_font) return 16;
|
||||
auto* cache = g_font->get_cache(size);
|
||||
return cache->ascent - cache->descent;
|
||||
}
|
||||
|
||||
static void px_button(uint32_t* px, int bw, int bh,
|
||||
int x, int y, int w, int h,
|
||||
const char* label, Color bg, Color fg, int r) {
|
||||
px_fill_rounded(px, bw, bh, x, y, w, h, r, bg);
|
||||
int tw = text_w(label);
|
||||
int fh = font_h();
|
||||
px_text(px, bw, bh, x + (w - tw) / 2, y + (h - fh) / 2, label, fg);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Volume helpers
|
||||
// ============================================================================
|
||||
|
||||
static void apply_volume(int vol) {
|
||||
if (vol < 0) vol = 0;
|
||||
if (vol > 100) vol = 100;
|
||||
g_volume = vol;
|
||||
montauk::audio_set_volume(0, g_volume);
|
||||
}
|
||||
|
||||
static void refresh_volume() {
|
||||
int v = montauk::audio_get_volume(0);
|
||||
if (v >= 0) g_volume = v;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Render
|
||||
// ============================================================================
|
||||
|
||||
static void render(uint32_t* pixels) {
|
||||
int fh_lg = font_h(FONT_SIZE_LG);
|
||||
|
||||
// Background
|
||||
px_fill(pixels, WIN_W, WIN_H, 0, 0, WIN_W, WIN_H, BG_COLOR);
|
||||
|
||||
// Volume percentage (large, centered)
|
||||
char vol_str[8];
|
||||
snprintf(vol_str, sizeof(vol_str), "%d%%", g_muted ? 0 : g_volume);
|
||||
int vw = text_w(vol_str, FONT_SIZE_LG);
|
||||
Color vol_color = g_muted ? MUTE_COLOR : ACCENT;
|
||||
px_text(pixels, WIN_W, WIN_H, (WIN_W - vw) / 2, 20, vol_str, vol_color, FONT_SIZE_LG);
|
||||
|
||||
// "Muted" label
|
||||
if (g_muted) {
|
||||
const char* muted_label = "Muted";
|
||||
int mw = text_w(muted_label);
|
||||
px_text(pixels, WIN_W, WIN_H, (WIN_W - mw) / 2,
|
||||
20 + fh_lg + 4, muted_label, MUTE_COLOR);
|
||||
}
|
||||
|
||||
// Slider track
|
||||
px_fill_rounded(pixels, WIN_W, WIN_H, SLIDER_X, SLIDER_Y, SLIDER_W, SLIDER_H, 4, TRACK_BG);
|
||||
|
||||
// Filled portion
|
||||
int display_vol = g_muted ? 0 : g_volume;
|
||||
int fill_w = (display_vol * SLIDER_W) / 100;
|
||||
if (fill_w > 0)
|
||||
px_fill_rounded(pixels, WIN_W, WIN_H, SLIDER_X, SLIDER_Y, fill_w, SLIDER_H, 4, ACCENT);
|
||||
|
||||
// Knob
|
||||
int knob_x = SLIDER_X + fill_w;
|
||||
int knob_y = SLIDER_Y + SLIDER_H / 2;
|
||||
px_circle(pixels, WIN_W, WIN_H, knob_x, knob_y, KNOB_R, ACCENT);
|
||||
px_circle(pixels, WIN_W, WIN_H, knob_x, knob_y, KNOB_R - 3, WHITE);
|
||||
|
||||
// Buttons: [-] and [+] and [Mute]
|
||||
int total_btn_w = BTN_W * 2 + 60 + 12 * 2; // minus, plus, mute + gaps
|
||||
int bx = (WIN_W - total_btn_w) / 2;
|
||||
|
||||
px_button(pixels, WIN_W, WIN_H, bx, BTN_Y, BTN_W, BTN_H,
|
||||
"-", Color::from_rgb(0xE0, 0xE0, 0xE0), TEXT_COLOR, BTN_RAD);
|
||||
bx += BTN_W + 12;
|
||||
|
||||
px_button(pixels, WIN_W, WIN_H, bx, BTN_Y, BTN_W, BTN_H,
|
||||
"+", Color::from_rgb(0xE0, 0xE0, 0xE0), TEXT_COLOR, BTN_RAD);
|
||||
bx += BTN_W + 12;
|
||||
|
||||
Color mute_bg = g_muted ? MUTE_COLOR : Color::from_rgb(0xE0, 0xE0, 0xE0);
|
||||
Color mute_fg = g_muted ? WHITE : TEXT_COLOR;
|
||||
px_button(pixels, WIN_W, WIN_H, bx, BTN_Y, 60, BTN_H,
|
||||
"Mute", mute_bg, mute_fg, BTN_RAD);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hit testing
|
||||
// ============================================================================
|
||||
|
||||
static int vol_from_slider_x(int mx) {
|
||||
int v = ((mx - SLIDER_X) * 100) / SLIDER_W;
|
||||
if (v < 0) v = 0;
|
||||
if (v > 100) v = 100;
|
||||
return v;
|
||||
}
|
||||
|
||||
static bool handle_click(int mx, int my) {
|
||||
// Slider area (generous vertical hit zone)
|
||||
if (my >= SLIDER_Y - KNOB_R && my <= SLIDER_Y + SLIDER_H + KNOB_R &&
|
||||
mx >= SLIDER_X - KNOB_R && mx <= SLIDER_X + SLIDER_W + KNOB_R) {
|
||||
g_muted = false;
|
||||
apply_volume(vol_from_slider_x(mx));
|
||||
g_dragging = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Buttons
|
||||
int total_btn_w = BTN_W * 2 + 60 + 12 * 2;
|
||||
int bx = (WIN_W - total_btn_w) / 2;
|
||||
|
||||
if (my >= BTN_Y && my < BTN_Y + BTN_H) {
|
||||
// [-] button
|
||||
if (mx >= bx && mx < bx + BTN_W) {
|
||||
g_muted = false;
|
||||
apply_volume(g_volume - 5);
|
||||
return true;
|
||||
}
|
||||
bx += BTN_W + 12;
|
||||
|
||||
// [+] button
|
||||
if (mx >= bx && mx < bx + BTN_W) {
|
||||
g_muted = false;
|
||||
apply_volume(g_volume + 5);
|
||||
return true;
|
||||
}
|
||||
bx += BTN_W + 12;
|
||||
|
||||
// [Mute] button
|
||||
if (mx >= bx && mx < bx + 60) {
|
||||
if (g_muted) {
|
||||
g_muted = false;
|
||||
apply_volume(g_pre_mute_vol);
|
||||
} else {
|
||||
g_pre_mute_vol = g_volume;
|
||||
g_muted = true;
|
||||
montauk::audio_set_volume(0, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Entry point
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void _start() {
|
||||
// Load font
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
refresh_volume();
|
||||
|
||||
// Create window
|
||||
Montauk::WinCreateResult wres;
|
||||
if (montauk::win_create("Volume", WIN_W, WIN_H, &wres) < 0 || wres.id < 0)
|
||||
montauk::exit(1);
|
||||
|
||||
int win_id = wres.id;
|
||||
uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
|
||||
|
||||
render(pixels);
|
||||
montauk::win_present(win_id);
|
||||
|
||||
while (true) {
|
||||
Montauk::WinEvent ev;
|
||||
int r = montauk::win_poll(win_id, &ev);
|
||||
|
||||
if (r < 0) break;
|
||||
if (r == 0) { montauk::sleep_ms(16); continue; }
|
||||
|
||||
bool redraw = false;
|
||||
|
||||
if (ev.type == 3) break; // close
|
||||
|
||||
// Keyboard
|
||||
if (ev.type == 0 && ev.key.pressed) {
|
||||
if (ev.key.scancode == 0x01) break; // Escape
|
||||
if (ev.key.scancode == 0x4D || ev.key.ascii == '+' || ev.key.ascii == '=') { // Right / +
|
||||
g_muted = false;
|
||||
apply_volume(g_volume + 5);
|
||||
redraw = true;
|
||||
} else if (ev.key.scancode == 0x4B || ev.key.ascii == '-') { // Left / -
|
||||
g_muted = false;
|
||||
apply_volume(g_volume - 5);
|
||||
redraw = true;
|
||||
} else if (ev.key.ascii == 'm' || ev.key.ascii == 'M') {
|
||||
if (g_muted) {
|
||||
g_muted = false;
|
||||
apply_volume(g_pre_mute_vol);
|
||||
} else {
|
||||
g_pre_mute_vol = g_volume;
|
||||
g_muted = true;
|
||||
montauk::audio_set_volume(0, 0);
|
||||
}
|
||||
redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse
|
||||
if (ev.type == 1) {
|
||||
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
bool released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1);
|
||||
|
||||
if (clicked) {
|
||||
if (handle_click(ev.mouse.x, ev.mouse.y))
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
if (g_dragging && (ev.mouse.buttons & 1)) {
|
||||
g_muted = false;
|
||||
apply_volume(vol_from_slider_x(ev.mouse.x));
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
if (released) {
|
||||
g_dragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (redraw) {
|
||||
render(pixels);
|
||||
montauk::win_present(win_id);
|
||||
}
|
||||
}
|
||||
|
||||
montauk::win_destroy(win_id);
|
||||
montauk::exit(0);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[app]
|
||||
name = "Volume"
|
||||
binary = "volume.elf"
|
||||
icon = "pavucontrol.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
visible = true
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* stb_truetype_impl.cpp
|
||||
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
// Override all stb_truetype dependencies before including the implementation
|
||||
|
||||
#define STBTT_ifloor(x) ((int) stb_floor(x))
|
||||
#define STBTT_iceil(x) ((int) stb_ceil(x))
|
||||
#define STBTT_sqrt(x) stb_sqrt(x)
|
||||
#define STBTT_pow(x,y) stb_pow(x,y)
|
||||
#define STBTT_fmod(x,y) stb_fmod(x,y)
|
||||
#define STBTT_cos(x) stb_cos(x)
|
||||
#define STBTT_acos(x) stb_acos(x)
|
||||
#define STBTT_fabs(x) stb_fabs(x)
|
||||
|
||||
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
|
||||
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
|
||||
|
||||
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
|
||||
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
|
||||
|
||||
#define STBTT_strlen(x) montauk::slen(x)
|
||||
|
||||
#define STBTT_assert(x) ((void)(x))
|
||||
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
Reference in New Issue
Block a user