桌面环境

This commit is contained in:
2026-04-25 17:53:47 +08:00
parent 4442101f28
commit 34521143c0
14 changed files with 855 additions and 31 deletions

View File

@@ -36,6 +36,11 @@ set(CLEONOS_QEMU_ACCEL_ARGS "")
if(CLEONOS_QEMU_ENABLE_KVM)
list(APPEND CLEONOS_QEMU_ACCEL_ARGS -enable-kvm -cpu host)
endif()
set(CLEONOS_QEMU_ENABLE_USB_TABLET ON CACHE BOOL "Enable QEMU USB tablet for absolute mouse pointer")
set(CLEONOS_QEMU_INPUT_ARGS "")
if(CLEONOS_QEMU_ENABLE_USB_TABLET)
list(APPEND CLEONOS_QEMU_INPUT_ARGS -usb -device usb-tablet)
endif()
set(CLEONOS_SOURCE_DIR "${CMAKE_SOURCE_DIR}/cleonos")
if(EXISTS "${CLEONOS_SOURCE_DIR}/CMakeLists.txt")
@@ -381,7 +386,7 @@ if(CLEONOS_ENABLE)
"-DLOG_LEVEL=STEP"
"-DLOG_TEXT=launching qemu run"
-P "${CL_LOG_EMIT_SCRIPT}"
COMMAND ${QEMU_X86_64} ${CLEONOS_QEMU_ACCEL_ARGS} -M pc -m 1024M -boot order=d -cdrom "${ISO_IMAGE}" -drive "file=${DISK_IMAGE},format=raw,if=none,id=clksdisk,media=disk" -device "ide-hd,drive=clksdisk,bus=ide.0" -netdev "user,id=clksnet0" -device "e1000,netdev=clksnet0" -serial stdio
COMMAND ${QEMU_X86_64} ${CLEONOS_QEMU_ACCEL_ARGS} ${CLEONOS_QEMU_INPUT_ARGS} -M pc -m 1024M -boot order=d -cdrom "${ISO_IMAGE}" -drive "file=${DISK_IMAGE},format=raw,if=none,id=clksdisk,media=disk" -device "ide-hd,drive=clksdisk,bus=ide.0" -netdev "user,id=clksnet0" -device "e1000,netdev=clksnet0" -serial stdio
DEPENDS iso disk-image
USES_TERMINAL
)
@@ -392,7 +397,7 @@ if(CLEONOS_ENABLE)
"-DLOG_LEVEL=STEP"
"-DLOG_TEXT=launching qemu debug (-s -S)"
-P "${CL_LOG_EMIT_SCRIPT}"
COMMAND ${QEMU_X86_64} ${CLEONOS_QEMU_ACCEL_ARGS} -M pc -m 1024M -boot order=d -cdrom "${ISO_IMAGE}" -drive "file=${DISK_IMAGE},format=raw,if=none,id=clksdisk,media=disk" -device "ide-hd,drive=clksdisk,bus=ide.0" -netdev "user,id=clksnet0" -device "e1000,netdev=clksnet0" -serial stdio -s -S
COMMAND ${QEMU_X86_64} ${CLEONOS_QEMU_ACCEL_ARGS} ${CLEONOS_QEMU_INPUT_ARGS} -M pc -m 1024M -boot order=d -cdrom "${ISO_IMAGE}" -drive "file=${DISK_IMAGE},format=raw,if=none,id=clksdisk,media=disk" -device "ide-hd,drive=clksdisk,bus=ide.0" -netdev "user,id=clksnet0" -device "e1000,netdev=clksnet0" -serial stdio -s -S
DEPENDS iso disk-image
USES_TERMINAL
)

View File

@@ -239,6 +239,9 @@ help:
> $(Q)echo "QEMU KVM toggle:"
> $(Q)echo " make run CMAKE_EXTRA_ARGS='-DCLEONOS_QEMU_ENABLE_KVM=ON' # force on"
> $(Q)echo " make run CMAKE_EXTRA_ARGS='-DCLEONOS_QEMU_ENABLE_KVM=OFF' # force off"
> $(Q)echo "QEMU USB tablet toggle:"
> $(Q)echo " make run CMAKE_EXTRA_ARGS='-DCLEONOS_QEMU_ENABLE_USB_TABLET=ON' # absolute pointer"
> $(Q)echo " make run CMAKE_EXTRA_ARGS='-DCLEONOS_QEMU_ENABLE_USB_TABLET=OFF' # disable tablet"
> $(Q)echo ""
> $(Q)echo "Pass custom CMake cache args via:"
> $(Q)echo " make configure CMAKE_EXTRA_ARGS='-DLIMINE_SKIP_CONFIGURE=1 -DOBJCOPY_FOR_TARGET=objcopy'"

View File

@@ -171,7 +171,7 @@ set(USER_SHELL_COMMAND_APPS
diskinfo mkfsfat32 mount partctl
shutdown restart exit clear ansi ansitest wavplay fastfetch memstat fsstat taskstat userstat
shstat stats tty dmesg kbdstat mkdir touch write append cp mv rm kdbg bmpview qrcode browser
vim
vim uwm
)
foreach(SRC IN LISTS USER_APP_MAIN_SOURCES)

View File

@@ -14,9 +14,10 @@ static int ush_cmd_help(void) {
ush_writeln(" clear");
ush_writeln(" ansi / ansitest / color");
ush_writeln(" bmpview <file.bmp> [cols]");
ush_writeln(" qrcode [--ecc <L|M|Q|H>] <text>");
ush_writeln(" vim [file] (vim-like editor: normal/insert/:w/:q/:wq)");
ush_writeln(" wavplay <file.wav> [steps] [ticks] / wavplay --stop");
ush_writeln(" qrcode [--ecc <L|M|Q|H>] <text>");
ush_writeln(" vim [file] (vim-like editor: normal/insert/:w/:q/:wq)");
ush_writeln(" uwm (user-space window manager demo)");
ush_writeln(" wavplay <file.wav> [steps] [ticks] / wavplay --stop");
ush_writeln(" fastfetch [--plain]");
ush_writeln(" doom [wad_path] (framebuffer bootstrap renderer)");
ush_writeln(" memstat / fsstat / taskstat / userstat / shstat / stats");

View File

@@ -137,6 +137,8 @@ static int ush_cmd_help(void) {
ush_writeln(" ansi / ansitest / color");
ush_writeln(" bmpview <file.bmp> [cols]");
ush_writeln(" qrcode [--ecc <L|M|Q|H>] <text>");
ush_writeln(" vim [file] (vim-like editor)");
ush_writeln(" uwm (user-space window manager demo)");
ush_writeln(" wavplay <file.wav> [steps] [ticks] / wavplay --stop");
ush_writeln(" fastfetch [--plain]");
ush_writeln(" doom [wad_path] (framebuffer bootstrap renderer)");

734
cleonos/c/apps/uwm_main.c Normal file
View File

@@ -0,0 +1,734 @@
#include "cmd_runtime.h"
#define USH_UWM_WINDOW_COUNT 3ULL
#define USH_UWM_TITLE_HEIGHT 18
#define USH_UWM_BORDER_THICKNESS 2
#define USH_UWM_WINDOW_MIN_W 120
#define USH_UWM_WINDOW_MIN_H 80
#define USH_UWM_STARTUP_KEY_DRAIN_MAX 256ULL
#define USH_UWM_BLIT_MAX_FAIL_STREAK 30ULL
#define USH_UWM_KEY_LEFT 1
#define USH_UWM_KEY_RIGHT 2
#define USH_UWM_KEY_UP 3
#define USH_UWM_KEY_DOWN 4
#define USH_UWM_COLOR_BG 0x00161D2BUL
#define USH_UWM_COLOR_PANEL 0x00263352UL
#define USH_UWM_COLOR_CURSOR 0x00FFFFFFUL
#define USH_UWM_COLOR_CURSOR_SHADOW 0x00000000UL
#define USH_UWM_COLOR_BORDER_ACTIVE 0x00FFD166UL
#define USH_UWM_COLOR_BORDER_INACTIVE 0x004A566EL
#define USH_UWM_COLOR_TITLE_ACTIVE 0x005A77B7UL
#define USH_UWM_COLOR_TITLE_INACTIVE 0x00394C78UL
typedef unsigned int ush_uwm_u32;
typedef struct ush_uwm_window {
int x;
int y;
int w;
int h;
ush_uwm_u32 body_color;
} ush_uwm_window;
typedef struct ush_uwm_runtime {
cleonos_fb_info fb;
ush_uwm_u32 *canvas;
u64 canvas_pixels;
int screen_w;
int screen_h;
int mouse_x;
int mouse_y;
u64 mouse_buttons;
int mouse_ready;
} ush_uwm_runtime;
static int ush_uwm_window_index_valid(int index) {
return (index >= 0 && index < (int)USH_UWM_WINDOW_COUNT) ? 1 : 0;
}
static void ush_uwm_z_order_sanitize(int *z_order) {
int seen[USH_UWM_WINDOW_COUNT];
int fill_index = 0;
u64 i;
if (z_order == (int *)0) {
return;
}
for (i = 0ULL; i < USH_UWM_WINDOW_COUNT; i++) {
seen[i] = 0;
}
for (i = 0ULL; i < USH_UWM_WINDOW_COUNT; i++) {
int value = z_order[i];
if (ush_uwm_window_index_valid(value) != 0 && seen[(u64)value] == 0) {
seen[(u64)value] = 1;
continue;
}
while (fill_index < (int)USH_UWM_WINDOW_COUNT && seen[(u64)fill_index] != 0) {
fill_index++;
}
if (fill_index >= (int)USH_UWM_WINDOW_COUNT) {
fill_index = (int)USH_UWM_WINDOW_COUNT - 1;
}
z_order[i] = fill_index;
seen[(u64)fill_index] = 1;
}
}
static int ush_uwm_runtime_sane(const ush_uwm_runtime *rt) {
u64 min_pixels;
if (rt == (const ush_uwm_runtime *)0 || rt->canvas == (ush_uwm_u32 *)0) {
return 0;
}
if (rt->screen_w <= 0 || rt->screen_h <= 0 || rt->screen_w > 4096 || rt->screen_h > 4096) {
return 0;
}
min_pixels = (u64)(unsigned int)rt->screen_w * (u64)(unsigned int)rt->screen_h;
if (rt->canvas_pixels < min_pixels) {
return 0;
}
return 1;
}
static int ush_uwm_clampi(int value, int min_value, int max_value) {
if (value < min_value) {
return min_value;
}
if (value > max_value) {
return max_value;
}
return value;
}
static int ush_uwm_u64_to_i32_sat(u64 value) {
if (value > 0x7FFFFFFFULL) {
return 0x7FFFFFFF;
}
return (int)value;
}
static void ush_uwm_fill_rect(ush_uwm_runtime *rt, int x, int y, int w, int h, ush_uwm_u32 color) {
int left;
int top;
int right;
int bottom;
int row;
if (ush_uwm_runtime_sane(rt) == 0 || w <= 0 || h <= 0) {
return;
}
left = x;
top = y;
right = x + w;
bottom = y + h;
if (left < 0) {
left = 0;
}
if (top < 0) {
top = 0;
}
if (right > rt->screen_w) {
right = rt->screen_w;
}
if (bottom > rt->screen_h) {
bottom = rt->screen_h;
}
if (left >= right || top >= bottom) {
return;
}
for (row = top; row < bottom; row++) {
u64 offset = ((u64)(unsigned int)row * (u64)(unsigned int)rt->screen_w) + (u64)(unsigned int)left;
u64 max_pixels = rt->canvas_pixels;
int col;
if (offset >= max_pixels) {
break;
}
for (col = left; col < right; col++) {
if (offset >= max_pixels) {
break;
}
rt->canvas[offset++] = color;
}
}
}
static void ush_uwm_draw_window(ush_uwm_runtime *rt, const ush_uwm_window *win, int active) {
ush_uwm_u32 border_color = (active != 0) ? USH_UWM_COLOR_BORDER_ACTIVE : USH_UWM_COLOR_BORDER_INACTIVE;
ush_uwm_u32 title_color = (active != 0) ? USH_UWM_COLOR_TITLE_ACTIVE : USH_UWM_COLOR_TITLE_INACTIVE;
int title_h;
if (rt == (ush_uwm_runtime *)0 || win == (const ush_uwm_window *)0) {
return;
}
title_h = USH_UWM_TITLE_HEIGHT;
if (title_h > win->h) {
title_h = win->h;
}
ush_uwm_fill_rect(rt, win->x, win->y, win->w, win->h, border_color);
ush_uwm_fill_rect(rt, win->x + USH_UWM_BORDER_THICKNESS, win->y + USH_UWM_BORDER_THICKNESS,
win->w - (USH_UWM_BORDER_THICKNESS * 2), title_h - USH_UWM_BORDER_THICKNESS, title_color);
ush_uwm_fill_rect(rt, win->x + USH_UWM_BORDER_THICKNESS, win->y + title_h, win->w - (USH_UWM_BORDER_THICKNESS * 2),
win->h - title_h - USH_UWM_BORDER_THICKNESS, win->body_color);
ush_uwm_fill_rect(rt, win->x + win->w - 16, win->y + 4, 10, 10, 0x00E76F51UL);
}
static void ush_uwm_draw_cursor(ush_uwm_runtime *rt, int x, int y) {
int i;
if (rt == (ush_uwm_runtime *)0) {
return;
}
for (i = -6; i <= 6; i++) {
ush_uwm_fill_rect(rt, x + i, y, 1, 1, (i == 0) ? USH_UWM_COLOR_CURSOR : USH_UWM_COLOR_CURSOR_SHADOW);
ush_uwm_fill_rect(rt, x, y + i, 1, 1, (i == 0) ? USH_UWM_COLOR_CURSOR : USH_UWM_COLOR_CURSOR_SHADOW);
}
ush_uwm_fill_rect(rt, x - 1, y - 1, 3, 3, USH_UWM_COLOR_CURSOR);
}
static void ush_uwm_draw_background(ush_uwm_runtime *rt) {
int y;
if (rt == (ush_uwm_runtime *)0) {
return;
}
ush_uwm_fill_rect(rt, 0, 0, rt->screen_w, rt->screen_h, USH_UWM_COLOR_BG);
ush_uwm_fill_rect(rt, 0, 0, rt->screen_w, 30, USH_UWM_COLOR_PANEL);
for (y = 60; y < rt->screen_h; y += 40) {
ush_uwm_fill_rect(rt, 0, y, rt->screen_w, 1, 0x001E283BUL);
}
}
static int ush_uwm_window_hit_title(const ush_uwm_window *win, int x, int y) {
int title_h;
if (win == (const ush_uwm_window *)0) {
return 0;
}
title_h = USH_UWM_TITLE_HEIGHT;
if (title_h > win->h) {
title_h = win->h;
}
if (x < win->x || y < win->y) {
return 0;
}
if (x >= win->x + win->w || y >= win->y + title_h) {
return 0;
}
return 1;
}
static void ush_uwm_bring_to_front(int *z_order, u64 count, int window_index) {
u64 i;
u64 pos = count;
if (z_order == (int *)0 || count == 0ULL || ush_uwm_window_index_valid(window_index) == 0) {
return;
}
for (i = 0ULL; i < count; i++) {
if (z_order[i] == window_index) {
pos = i;
break;
}
}
if (pos >= count || pos + 1ULL >= count) {
return;
}
for (i = pos; i + 1ULL < count; i++) {
z_order[i] = z_order[i + 1ULL];
}
z_order[count - 1ULL] = window_index;
}
static int ush_uwm_cycle_focus(const int *z_order, u64 count, int active_window) {
u64 i;
u64 pos = 0ULL;
if (z_order == (const int *)0 || count == 0ULL) {
return 0;
}
if (ush_uwm_window_index_valid(active_window) == 0) {
return z_order[count - 1ULL];
}
for (i = 0ULL; i < count; i++) {
if (z_order[i] == active_window) {
pos = i;
break;
}
}
return z_order[(pos + 1ULL) % count];
}
static int ush_uwm_poll_mouse(ush_uwm_runtime *rt) {
cleonos_mouse_state ms;
int max_x;
int max_y;
if (rt == (ush_uwm_runtime *)0) {
return 0;
}
ush_zero(&ms, (u64)sizeof(ms));
if (cleonos_sys_mouse_state(&ms) == 0ULL) {
return 0;
}
rt->mouse_buttons = ms.buttons;
rt->mouse_ready = (ms.ready != 0ULL) ? 1 : 0;
if (rt->screen_w <= 0 || rt->screen_h <= 0) {
return rt->mouse_ready;
}
max_x = rt->screen_w - 1;
max_y = rt->screen_h - 1;
rt->mouse_x = ush_uwm_clampi(ush_uwm_u64_to_i32_sat(ms.x), 0, max_x);
rt->mouse_y = ush_uwm_clampi(ush_uwm_u64_to_i32_sat(ms.y), 0, max_y);
return rt->mouse_ready;
}
static int ush_uwm_present(ush_uwm_runtime *rt) {
cleonos_fb_blit_req req;
if (ush_uwm_runtime_sane(rt) == 0) {
return 0;
}
req.pixels_ptr = (u64)(usize)rt->canvas;
req.src_width = (u64)(unsigned int)rt->screen_w;
req.src_height = (u64)(unsigned int)rt->screen_h;
req.src_pitch_bytes = (u64)(unsigned int)rt->screen_w * 4ULL;
req.dst_x = 0ULL;
req.dst_y = 0ULL;
req.scale = 1ULL;
return (cleonos_sys_fb_blit(&req) != 0ULL) ? 1 : 0;
}
static u64 ush_uwm_drain_startup_keys(void) {
u64 drained = 0ULL;
while (drained < USH_UWM_STARTUP_KEY_DRAIN_MAX) {
u64 key = cleonos_sys_kbd_get_char();
if (key == (u64)-1) {
break;
}
drained++;
}
return drained;
}
/* return: 0 fail, 1 ok, 2 help */
static int ush_uwm_parse_args(const char *arg) {
char first[USH_PATH_MAX];
const char *rest = "";
if (arg == (const char *)0 || arg[0] == '\0') {
return 1;
}
if (ush_split_first_and_rest(arg, first, (u64)sizeof(first), &rest) == 0) {
return 0;
}
if (rest != (const char *)0 && rest[0] != '\0') {
return 0;
}
if (ush_streq(first, "--help") != 0 || ush_streq(first, "-h") != 0) {
return 2;
}
return 0;
}
static void ush_uwm_usage(void) {
ush_writeln("usage: uwm");
ush_writeln("keys: q quit, tab cycle focus, wasd move active window");
ush_writeln("mouse: drag window by title bar");
}
static int ush_cmd_uwm(const char *arg) {
ush_uwm_runtime rt;
ush_uwm_window windows[USH_UWM_WINDOW_COUNT];
int z_order[USH_UWM_WINDOW_COUNT];
int active_window = 0;
int dragging = 0;
int drag_window = 0;
int drag_offset_x = 0;
int drag_offset_y = 0;
u64 prev_buttons = 0ULL;
u64 blit_fail_streak = 0ULL;
int running = 1;
int parse_result;
u64 drained_keys;
parse_result = ush_uwm_parse_args(arg);
if (parse_result == 2) {
ush_uwm_usage();
return 1;
}
if (parse_result == 0) {
ush_uwm_usage();
return 0;
}
ush_zero(&rt, (u64)sizeof(rt));
if (cleonos_sys_fb_info(&rt.fb) == 0ULL || rt.fb.width == 0ULL || rt.fb.height == 0ULL || rt.fb.bpp != 32ULL) {
ush_writeln("uwm: framebuffer unavailable");
return 0;
}
if (rt.fb.width > 4096ULL || rt.fb.height > 4096ULL) {
ush_writeln("uwm: framebuffer too large");
return 0;
}
rt.screen_w = (int)rt.fb.width;
rt.screen_h = (int)rt.fb.height;
rt.canvas_pixels = rt.fb.width * rt.fb.height;
rt.canvas = (ush_uwm_u32 *)malloc((size_t)(rt.canvas_pixels * 4ULL));
if (rt.canvas == (ush_uwm_u32 *)0) {
ush_writeln("uwm: framebuffer buffer alloc failed");
return 0;
}
windows[0].x = 50;
windows[0].y = 70;
windows[0].w = rt.screen_w / 3;
windows[0].h = rt.screen_h / 3;
windows[0].body_color = 0x002C3E6BUL;
windows[1].x = 180;
windows[1].y = 140;
windows[1].w = rt.screen_w / 3;
windows[1].h = rt.screen_h / 3;
windows[1].body_color = 0x003F5B7EUL;
windows[2].x = 310;
windows[2].y = 210;
windows[2].w = rt.screen_w / 3;
windows[2].h = rt.screen_h / 3;
windows[2].body_color = 0x00506F89UL;
z_order[0] = 0;
z_order[1] = 1;
z_order[2] = 2;
active_window = 2;
{
u64 i;
for (i = 0ULL; i < USH_UWM_WINDOW_COUNT; i++) {
int max_w = rt.screen_w - 20;
int max_h = rt.screen_h - 40;
if (max_w < USH_UWM_WINDOW_MIN_W) {
max_w = USH_UWM_WINDOW_MIN_W;
}
if (max_h < USH_UWM_WINDOW_MIN_H) {
max_h = USH_UWM_WINDOW_MIN_H;
}
if (windows[i].w < USH_UWM_WINDOW_MIN_W) {
windows[i].w = USH_UWM_WINDOW_MIN_W;
}
if (windows[i].h < USH_UWM_WINDOW_MIN_H) {
windows[i].h = USH_UWM_WINDOW_MIN_H;
}
if (windows[i].w > max_w) {
windows[i].w = max_w;
}
if (windows[i].h > max_h) {
windows[i].h = max_h;
}
}
}
rt.mouse_x = rt.screen_w / 2;
rt.mouse_y = rt.screen_h / 2;
rt.mouse_buttons = 0ULL;
rt.mouse_ready = 0;
ush_writeln("uwm: enter user window manager");
ush_writeln("uwm: q quit, tab focus, wasd move, drag title bar with mouse");
(void)cleonos_sys_fb_clear(USH_UWM_COLOR_BG);
drained_keys = ush_uwm_drain_startup_keys();
if (drained_keys > 0ULL) {
ush_writeln("uwm: stale keyboard input discarded");
}
while (running != 0) {
u64 key;
int left_down;
int left_press;
int left_release;
if (ush_uwm_runtime_sane(&rt) == 0) {
free(rt.canvas);
ush_writeln("uwm: runtime state corrupted");
return 0;
}
ush_uwm_z_order_sanitize(z_order);
if (ush_uwm_window_index_valid(active_window) == 0) {
active_window = z_order[USH_UWM_WINDOW_COUNT - 1ULL];
}
if (ush_uwm_window_index_valid(drag_window) == 0) {
dragging = 0;
drag_window = active_window;
}
(void)ush_uwm_poll_mouse(&rt);
for (;;) {
key = cleonos_sys_kbd_get_char();
if (key == (u64)-1) {
break;
}
if (key == (u64)'q' || key == (u64)'Q') {
running = 0;
break;
}
if (key == 27ULL) {
continue;
}
if (key == (u64)'\t') {
active_window = ush_uwm_cycle_focus(z_order, USH_UWM_WINDOW_COUNT, active_window);
if (ush_uwm_window_index_valid(active_window) != 0) {
ush_uwm_bring_to_front(z_order, USH_UWM_WINDOW_COUNT, active_window);
}
continue;
}
if (ush_uwm_window_index_valid(active_window) == 0) {
continue;
}
if (key == (u64)'a' || key == (u64)'A') {
windows[active_window].x -= 8;
} else if (key == (u64)'d' || key == (u64)'D') {
windows[active_window].x += 8;
} else if (key == (u64)'w' || key == (u64)'W') {
windows[active_window].y -= 8;
} else if (key == (u64)'s' || key == (u64)'S') {
windows[active_window].y += 8;
} else if (key == USH_UWM_KEY_LEFT) {
rt.mouse_x -= 10;
} else if (key == USH_UWM_KEY_RIGHT) {
rt.mouse_x += 10;
} else if (key == USH_UWM_KEY_UP) {
rt.mouse_y -= 10;
} else if (key == USH_UWM_KEY_DOWN) {
rt.mouse_y += 10;
}
}
if (running == 0) {
break;
}
left_down = ((rt.mouse_buttons & 0x01ULL) != 0ULL) ? 1 : 0;
left_press = (left_down != 0 && (prev_buttons & 0x01ULL) == 0ULL) ? 1 : 0;
left_release = (left_down == 0 && (prev_buttons & 0x01ULL) != 0ULL) ? 1 : 0;
if (left_press != 0) {
int hit = -1;
i64 z;
for (z = (i64)USH_UWM_WINDOW_COUNT - 1LL; z >= 0LL; z--) {
int win_index = z_order[(u64)z];
if (ush_uwm_window_index_valid(win_index) == 0) {
continue;
}
if (ush_uwm_window_hit_title(&windows[win_index], rt.mouse_x, rt.mouse_y) != 0) {
hit = win_index;
break;
}
}
if (hit >= 0) {
active_window = hit;
ush_uwm_bring_to_front(z_order, USH_UWM_WINDOW_COUNT, hit);
dragging = 1;
drag_window = hit;
drag_offset_x = rt.mouse_x - windows[hit].x;
drag_offset_y = rt.mouse_y - windows[hit].y;
}
}
if (left_release != 0) {
dragging = 0;
}
if (dragging != 0 && left_down != 0 && ush_uwm_window_index_valid(drag_window) != 0) {
int new_x = rt.mouse_x - drag_offset_x;
int new_y = rt.mouse_y - drag_offset_y;
int max_x = rt.screen_w - windows[drag_window].w;
int max_y = rt.screen_h - windows[drag_window].h;
int min_y = (max_y >= 30) ? 30 : 0;
if (max_x < 0) {
max_x = 0;
}
if (max_y < 0) {
max_y = 0;
}
windows[drag_window].x = ush_uwm_clampi(new_x, 0, max_x);
windows[drag_window].y = ush_uwm_clampi(new_y, min_y, max_y);
}
if (ush_uwm_window_index_valid(active_window) != 0) {
int max_x = rt.screen_w - windows[active_window].w;
int max_y = rt.screen_h - windows[active_window].h;
int min_y = (max_y >= 30) ? 30 : 0;
if (max_x < 0) {
max_x = 0;
}
if (max_y < 0) {
max_y = 0;
}
windows[active_window].x = ush_uwm_clampi(windows[active_window].x, 0, max_x);
windows[active_window].y = ush_uwm_clampi(windows[active_window].y, min_y, max_y);
}
rt.mouse_x = ush_uwm_clampi(rt.mouse_x, 0, rt.screen_w - 1);
rt.mouse_y = ush_uwm_clampi(rt.mouse_y, 0, rt.screen_h - 1);
ush_uwm_draw_background(&rt);
{
u64 i;
for (i = 0ULL; i < USH_UWM_WINDOW_COUNT; i++) {
int win_index = z_order[i];
if (ush_uwm_window_index_valid(win_index) == 0) {
continue;
}
int is_active = (win_index == active_window) ? 1 : 0;
ush_uwm_draw_window(&rt, &windows[win_index], is_active);
}
}
ush_uwm_draw_cursor(&rt, rt.mouse_x, rt.mouse_y);
if (ush_uwm_present(&rt) == 0) {
blit_fail_streak++;
if (blit_fail_streak == 1ULL) {
ush_writeln("uwm: framebuffer blit failed, retrying");
}
if (blit_fail_streak >= USH_UWM_BLIT_MAX_FAIL_STREAK) {
free(rt.canvas);
ush_writeln("uwm: framebuffer blit failed too many times");
return 0;
}
(void)cleonos_sys_sleep_ticks(1ULL);
continue;
}
blit_fail_streak = 0ULL;
prev_buttons = rt.mouse_buttons;
(void)cleonos_sys_sleep_ticks(1ULL);
}
(void)cleonos_sys_fb_clear(0x00000000ULL);
free(rt.canvas);
ush_writeln("uwm: exit");
return 1;
}
int cleonos_app_main(int argc, char **argv, char **envp) {
ush_cmd_ctx ctx;
ush_cmd_ret ret;
ush_state sh;
char initial_cwd[USH_PATH_MAX];
char arg_buf[USH_ARG_MAX];
int has_context = 0;
int success = 0;
const char *arg = "";
(void)envp;
ush_zero(&ctx, (u64)sizeof(ctx));
ush_zero(&ret, (u64)sizeof(ret));
ush_zero(arg_buf, (u64)sizeof(arg_buf));
ush_init_state(&sh);
ush_copy(initial_cwd, (u64)sizeof(initial_cwd), sh.cwd);
if (ush_command_ctx_read(&ctx) != 0) {
if (ctx.cmd[0] != '\0' && ush_streq(ctx.cmd, "uwm") != 0) {
has_context = 1;
arg = ctx.arg;
if (ctx.cwd[0] == '/') {
ush_copy(sh.cwd, (u64)sizeof(sh.cwd), ctx.cwd);
ush_copy(initial_cwd, (u64)sizeof(initial_cwd), sh.cwd);
}
}
}
if (has_context == 0 && argc > 1 && argv != (char **)0 && argv[1] != (char *)0) {
ush_copy(arg_buf, (u64)sizeof(arg_buf), argv[1]);
arg = arg_buf;
}
success = ush_cmd_uwm(arg);
if (has_context != 0) {
if (ush_streq(sh.cwd, initial_cwd) == 0) {
ret.flags |= USH_CMD_RET_FLAG_CWD;
ush_copy(ret.cwd, (u64)sizeof(ret.cwd), sh.cwd);
}
if (sh.exit_requested != 0) {
ret.flags |= USH_CMD_RET_FLAG_EXIT;
ret.exit_code = sh.exit_code;
}
(void)ush_command_ret_write(&ret);
}
return (success != 0) ? 0 : 1;
}

View File

@@ -43,12 +43,20 @@ typedef struct cleonos_proc_snapshot {
char path[CLEONOS_PROC_PATH_MAX];
} cleonos_proc_snapshot;
typedef struct cleonos_fb_info {
u64 width;
u64 height;
u64 pitch;
u64 bpp;
} cleonos_fb_info;
typedef struct cleonos_fb_info {
u64 width;
u64 height;
u64 pitch;
u64 bpp;
} cleonos_fb_info;
typedef struct cleonos_mouse_state {
u64 x;
u64 y;
u64 buttons;
u64 packet_count;
u64 ready;
} cleonos_mouse_state;
typedef struct cleonos_fb_blit_req {
u64 pixels_ptr;
@@ -198,10 +206,11 @@ typedef struct cleonos_net_tcp_recv_req {
#define CLEONOS_SYSCALL_NET_NETMASK 100ULL
#define CLEONOS_SYSCALL_NET_GATEWAY 101ULL
#define CLEONOS_SYSCALL_NET_DNS_SERVER 102ULL
#define CLEONOS_SYSCALL_NET_TCP_CONNECT 103ULL
#define CLEONOS_SYSCALL_NET_TCP_SEND 104ULL
#define CLEONOS_SYSCALL_NET_TCP_RECV 105ULL
#define CLEONOS_SYSCALL_NET_TCP_CLOSE 106ULL
#define CLEONOS_SYSCALL_NET_TCP_CONNECT 103ULL
#define CLEONOS_SYSCALL_NET_TCP_SEND 104ULL
#define CLEONOS_SYSCALL_NET_TCP_RECV 105ULL
#define CLEONOS_SYSCALL_NET_TCP_CLOSE 106ULL
#define CLEONOS_SYSCALL_MOUSE_STATE 107ULL
u64 cleonos_syscall(u64 id, u64 arg0, u64 arg1, u64 arg2);
u64 cleonos_sys_log_write(const char *message, u64 length);
@@ -307,9 +316,10 @@ u64 cleonos_sys_net_dns_server(void);
u64 cleonos_sys_net_ping(u64 dst_ipv4_be, u64 poll_budget);
u64 cleonos_sys_net_udp_send(const cleonos_net_udp_send_req *req);
u64 cleonos_sys_net_udp_recv(cleonos_net_udp_recv_req *req);
u64 cleonos_sys_net_tcp_connect(const cleonos_net_tcp_connect_req *req);
u64 cleonos_sys_net_tcp_send(const cleonos_net_tcp_send_req *req);
u64 cleonos_sys_net_tcp_recv(cleonos_net_tcp_recv_req *req);
u64 cleonos_sys_net_tcp_close(u64 poll_budget);
#endif
u64 cleonos_sys_net_tcp_connect(const cleonos_net_tcp_connect_req *req);
u64 cleonos_sys_net_tcp_send(const cleonos_net_tcp_send_req *req);
u64 cleonos_sys_net_tcp_recv(cleonos_net_tcp_recv_req *req);
u64 cleonos_sys_net_tcp_close(u64 poll_budget);
u64 cleonos_sys_mouse_state(cleonos_mouse_state *out_state);
#endif

View File

@@ -456,6 +456,10 @@ u64 cleonos_sys_net_tcp_recv(cleonos_net_tcp_recv_req *req) {
return cleonos_syscall(CLEONOS_SYSCALL_NET_TCP_RECV, (u64)req, 0ULL, 0ULL);
}
u64 cleonos_sys_net_tcp_close(u64 poll_budget) {
return cleonos_syscall(CLEONOS_SYSCALL_NET_TCP_CLOSE, poll_budget, 0ULL, 0ULL);
}
u64 cleonos_sys_net_tcp_close(u64 poll_budget) {
return cleonos_syscall(CLEONOS_SYSCALL_NET_TCP_CLOSE, poll_budget, 0ULL, 0ULL);
}
u64 cleonos_sys_mouse_state(cleonos_mouse_state *out_state) {
return cleonos_syscall(CLEONOS_SYSCALL_MOUSE_STATE, (u64)out_state, 0ULL, 0ULL);
}

2
clks

Submodule clks updated: 6ac3f4e696...96a0da2b1d

View File

@@ -26,9 +26,10 @@
- `stage27.md`
- `stage28.md`
- `stage29.md`
- `stage30.md`
- `syscall.md` (syscall ABI reference)
## Notes
- Stage docs use a fixed template: goal, implementation, acceptance criteria, build targets, QEMU command, and debugging notes.
- Stages 16~19 are currently not documented in this folder; add them later using the same template to keep history continuous.
- Stages 16~19 are currently not documented in this folder; add them later using the same template to keep history continuous.

52
docs/stage30.md Normal file
View File

@@ -0,0 +1,52 @@
# Stage 30 - User-Space Window Manager (UWM) + Mouse Syscall
## Goal
- Add a minimal user-space desktop environment entrypoint.
- Provide a simple window manager demo (`uwm`) with focus, dragging, and rendering in framebuffer.
- Expose mouse snapshot data to user-space through a dedicated syscall.
## Implementation
- New syscall:
- Added `MOUSE_STATE` (`id=107`) in kernel/user syscall headers.
- Kernel side (`clks/kernel/runtime/syscall.c`) now exports mouse snapshot:
- fields: `x`, `y`, `buttons`, `packet_count`, `ready`.
- User wrapper added: `cleonos_sys_mouse_state()`.
- New user app:
- Added `cleonos/c/apps/uwm_main.c`.
- Features:
- Desktop background rendering.
- Multiple windows with z-order.
- Active window focus + border highlight.
- Drag by title bar (left mouse).
- Keyboard fallback controls (`Tab`, `W/A/S/D`, `Q/Esc`).
- Build integration:
- Added `uwm` into shell command app set in `cleonos/CMakeLists.txt`.
- Updated shell help text and standalone `help` app output.
- QEMU run/debug update:
- Added optional absolute pointer device by default:
- `-usb -device usb-tablet`
- New CMake option:
- `CLEONOS_QEMU_ENABLE_USB_TABLET=ON` (default).
## Acceptance Criteria
- `uwm` can be started from user shell.
- Framebuffer window demo draws multiple windows and supports dragging.
- Mouse state is retrievable from user-space via syscall `107`.
- `make run`/`make debug` use USB tablet by default for smoother pointer interaction.
## Build Targets
- `make userapps`
- `make iso`
- `make run`
## QEMU Command
- `make run`
## Debug Notes
- If `uwm` reports framebuffer unavailable:
- verify `FB_INFO` syscall is enabled and framebuffer bpp is `32`.
- If pointer behavior is relative/jumpy:
- keep `CLEONOS_QEMU_ENABLE_USB_TABLET=ON`.
- for manual QEMU invocation, include `-usb -device usb-tablet`.
- If mouse syscall returns `ready=0`:
- check boot log for PS/2 mouse init status under `[INFO][MOUSE]`.

View File

@@ -83,7 +83,7 @@ u64 cleonos_syscall(u64 id, u64 arg0, u64 arg1, u64 arg2);
- `/proc/<pid>`:指定 PID 快照文本
- `/proc` 为只读;写入类 syscall 不支持。
## 4. Syscall 列表0~106
## 4. Syscall 列表0~107
### 0 `CLEONOS_SYSCALL_LOG_WRITE`
@@ -808,6 +808,16 @@ u64 cleonos_syscall(u64 id, u64 arg0, u64 arg1, u64 arg2);
- `arg0`: `u64 poll_budget`
- 返回:关闭成功返回 `1`,失败返回 `0`
- 说明:最小关闭流程支持 FIN/ACK 轮询等待,超时会强制回收连接状态。
### 107 `CLEONOS_SYSCALL_MOUSE_STATE`
- 参数:
- `arg0`: `struct { u64 x; u64 y; u64 buttons; u64 packet_count; u64 ready; } *out_state`
- 返回:成功返回 `1`,失败返回 `0`
- 说明:
- 读取当前鼠标状态快照,坐标为 framebuffer 像素坐标系(左上角为原点)。
- `buttons` 位掩码:`bit0=left``bit1=right``bit2=middle`
- `ready=1` 表示鼠标设备已上线;`ready=0` 时坐标/按键可能为默认值。
## 5. 用户态封装函数
@@ -844,6 +854,7 @@ u64 cleonos_syscall(u64 id, u64 arg0, u64 arg1, u64 arg2);
- `cleonos_sys_net_available()` / `cleonos_sys_net_ipv4_addr()` / `cleonos_sys_net_netmask()` / `cleonos_sys_net_gateway()` / `cleonos_sys_net_dns_server()` / `cleonos_sys_net_ping()`
- `cleonos_sys_net_udp_send()` / `cleonos_sys_net_udp_recv()`
- `cleonos_sys_net_tcp_connect()` / `cleonos_sys_net_tcp_send()` / `cleonos_sys_net_tcp_recv()` / `cleonos_sys_net_tcp_close()`
- `cleonos_sys_mouse_state()`
## 6. 开发注意事项
@@ -854,7 +865,7 @@ u64 cleonos_syscall(u64 id, u64 arg0, u64 arg1, u64 arg2);
## 7. Wine 兼容说明
- `wine/cleonos_wine_lib/runner.py` 当前已覆盖到 `0..106`(含 `DL_*``FB_*``KERNEL_VERSION``DISK_*``NET_*`)。
- `wine/cleonos_wine_lib/runner.py` 当前已覆盖到 `0..107`(含 `DL_*``FB_*``KERNEL_VERSION``DISK_*``NET_*``MOUSE_STATE`)。
- `DL_*``77..79`)在 Wine 中为“可运行兼容”实现:
- `DL_OPEN`:加载 guest ELF 到当前 Unicorn 地址空间,返回稳定 `handle`,并做引用计数。
- `DL_SYM`:解析 ELF `SYMTAB/DYNSYM` 并返回 guest 可调用地址。
@@ -872,6 +883,7 @@ u64 cleonos_syscall(u64 id, u64 arg0, u64 arg1, u64 arg2);
- `DISK_MOUNT`/`DISK_MOUNT_PATH` 支持挂载点管理,并与 `FS_MKDIR/WRITE/APPEND/REMOVE` 的路径规则联动。
- `DISK_READ_SECTOR`/`DISK_WRITE_SECTOR``93..94`)在 Wine 中已实现为 512B 原始扇区读写host 文件后端)。
- 网络 syscall`95..106`)在 Wine 当前为兼容占位实现(统一返回 `0`);即 Wine 运行模式下不会提供真实网络收发。
- `MOUSE_STATE``107`)在 Wine 中为基础兼容实现:可返回指针数据结构;未启用窗口鼠标事件时 `ready` 可能为 `0`
- Wine 在运行时崩溃场景下会生成与内核一致格式的“信号编码退出状态”,可通过 `WAITPID` 读取。
- Wine 当前音频 syscall 为占位实现:`AUDIO_AVAILABLE=0``AUDIO_PLAY_TONE=0``AUDIO_STOP=1`
- Wine 版本号策略固定为 `85.0.0-wine`(历史兼容号;不会随 syscall 扩展继续增长)。

2
kit

Submodule kit updated: 6be905609a...1e7b5aa8d2

2
wine

Submodule wine updated: abf4a53e78...6ffb354265