diff --git a/include/kernel/editor.h b/include/kernel/editor.h index 6ed1dc2..1046ce5 100644 --- a/include/kernel/editor.h +++ b/include/kernel/editor.h @@ -3,6 +3,7 @@ #include #include +#include #define EDITOR_MAX_LINES 128 #define EDITOR_LINE_LENGTH 128 @@ -25,6 +26,9 @@ class Editor { void exit(bool save); void render(); void draw_line(const char* text, int y, bool active_line); + void put_cell(size_t x, size_t y, char ch, uint8_t color); + void present_window(); + void update_cursor_visual(size_t row, size_t column, bool active); void handle_char(char c); void handle_enter(); @@ -48,6 +52,7 @@ class Editor { bool active; char status_message[EDITOR_LINE_LENGTH]; // status bar message + Process* owner_proc = nullptr; }; void editor_start(const char* path); diff --git a/include/kernel/graphics.h b/include/kernel/graphics.h new file mode 100644 index 0000000..111038d --- /dev/null +++ b/include/kernel/graphics.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include + +namespace graphics +{ +using Color = uint8_t; + +// Construct a VGA color value compatible with text-mode windows. +Color make_color(vga_color foreground, vga_color background); + +size_t columns(); +size_t rows(); + +// Ensure the current process owns a window before drawing. +void ensure_window(); + +void clear(Color color, char fill_char = ' '); + +void put_char(size_t column, size_t row, char ch, Color color); +void draw_text(size_t column, size_t row, const char *text, Color color); +void fill_rect(size_t column, size_t row, size_t width, size_t height, char ch, Color color); + +void set_cursor(size_t row, size_t column, bool active); +bool get_cursor(size_t &row, size_t &column); + +void present(); +} // namespace graphics + diff --git a/include/kernel/gui.h b/include/kernel/gui.h index 17911b3..c9b9ee0 100644 --- a/include/kernel/gui.h +++ b/include/kernel/gui.h @@ -2,6 +2,7 @@ #include #include +#include class Terminal; struct Process; @@ -16,4 +17,9 @@ uint32_t background_color_for_row(uint32_t y); void fill_background_rect(uint32_t x, uint32_t y, uint32_t width, uint32_t height); void set_background_fill_override(uint32_t color); void clear_background_fill_override(); + +void initialize_mouse_cursor(int32_t x, int32_t y, uint8_t buttons); +void handle_mouse_event(const MouseEvent &event, Terminal &terminal); +void begin_window_redraw(); +void end_window_redraw(); } diff --git a/include/kernel/hooks.h b/include/kernel/hooks.h index afaa461..bb6dc25 100644 --- a/include/kernel/hooks.h +++ b/include/kernel/hooks.h @@ -1,4 +1,4 @@ - +#pragma once #include enum class HookType { diff --git a/include/kernel/mouse.h b/include/kernel/mouse.h index 6901796..32e20cc 100644 --- a/include/kernel/mouse.h +++ b/include/kernel/mouse.h @@ -1,3 +1,39 @@ #pragma once +#include + +#define MOUSE_BUTTON_LEFT 0x01u +#define MOUSE_BUTTON_RIGHT 0x02u +#define MOUSE_BUTTON_MIDDLE 0x04u + +typedef struct MouseEvent +{ + int32_t x; + int32_t y; + int16_t dx; + int16_t dy; + int8_t scroll_x; + int8_t scroll_y; + uint8_t buttons; + uint8_t changed; + int32_t target_pid; +} MouseEvent; + +typedef struct MouseState +{ + int32_t x; + int32_t y; + uint8_t buttons; + uint8_t available; +} MouseState; + +#ifdef __cplusplus +extern "C" { +#endif + void mouse_initialize(); +MouseState mouse_get_state(); + +#ifdef __cplusplus +} +#endif diff --git a/include/kernel/terminal_windows.h b/include/kernel/terminal_windows.h index 9b83c87..e2995b3 100644 --- a/include/kernel/terminal_windows.h +++ b/include/kernel/terminal_windows.h @@ -2,6 +2,7 @@ #include #include +#include class Terminal; struct Process; @@ -15,4 +16,9 @@ void activate_process(Process *proc, Terminal &terminal); void set_active_window_origin(Terminal &terminal, Process *proc, int32_t x, int32_t y); void on_process_exit(Process *proc, Terminal &terminal); void write_text(Terminal &terminal, Process *proc, const char *text, size_t length); +bool handle_mouse_event(Terminal &terminal, const MouseEvent &event); +void window_put_char(Process *proc, size_t x, size_t y, char ch, uint8_t color); +void window_set_cursor(Process *proc, size_t row, size_t column, bool active); +void window_present(Process *proc); +bool window_get_cursor(Process *proc, size_t &row, size_t &column); } diff --git a/libc/include/sys/events.h b/libc/include/sys/events.h index c724099..b87ce76 100644 --- a/libc/include/sys/events.h +++ b/libc/include/sys/events.h @@ -3,6 +3,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { @@ -11,6 +12,7 @@ extern "C" { typedef enum { EVENT_NONE = 0, EVENT_KEYBOARD, + EVENT_MOUSE, EVENT_PROCESS, EVENT_PCI, } EventType; @@ -35,6 +37,7 @@ typedef struct { EventType type; union { keyboard_event keyboard; + MouseEvent mouse; process_event_data process; pci_event_data pci; } data; diff --git a/src/kernel/graphics.cpp b/src/kernel/graphics.cpp new file mode 100644 index 0000000..7159ae3 --- /dev/null +++ b/src/kernel/graphics.cpp @@ -0,0 +1,210 @@ +#include + +#include +#include +#include +#include + +extern Terminal terminal; + +namespace graphics +{ +namespace +{ +Process *current_process() +{ + return scheduler_current_process(); +} + +bool ensure_window_for(Process *proc) +{ + if (proc == nullptr || !framebuffer::is_available()) + { + return false; + } + + size_t dummy_row = 0; + size_t dummy_col = 0; + if (!terminal_windows::window_get_cursor(proc, dummy_row, dummy_col)) + { + terminal_windows::request_new_window(terminal, proc); + } + + return terminal_windows::window_get_cursor(proc, dummy_row, dummy_col); +} +} // namespace + +Color make_color(vga_color foreground, vga_color background) +{ + return terminal.make_color(foreground, background); +} + +size_t columns() +{ + return Terminal::VGA_WIDTH; +} + +size_t rows() +{ + return Terminal::VGA_HEIGHT; +} + +void ensure_window() +{ + (void)ensure_window_for(current_process()); +} + +void clear(Color color, char fill_char) +{ + Process *proc = current_process(); + if (!ensure_window_for(proc)) + { + return; + } + + const size_t max_columns = columns(); + const size_t max_rows = rows(); + + for (size_t row = 0; row < max_rows; ++row) + { + for (size_t col = 0; col < max_columns; ++col) + { + terminal_windows::window_put_char(proc, col, row, fill_char, color); + } + } + + terminal_windows::window_set_cursor(proc, 0, 0, false); +} + +void put_char(size_t column, size_t row, char ch, Color color) +{ + Process *proc = current_process(); + if (!ensure_window_for(proc)) + { + return; + } + + if (column >= columns() || row >= rows()) + { + return; + } + + terminal_windows::window_put_char(proc, column, row, ch, color); +} + +void draw_text(size_t column, size_t row, const char *text, Color color) +{ + if (text == nullptr) + { + return; + } + + Process *proc = current_process(); + if (!ensure_window_for(proc)) + { + return; + } + + size_t cursor_col = column; + size_t cursor_row = row; + const size_t max_columns = columns(); + const size_t max_rows = rows(); + + for (size_t i = 0; text[i] != '\0'; ++i) + { + char ch = text[i]; + + if (ch == '\n') + { + cursor_col = column; + if (++cursor_row >= max_rows) + { + break; + } + continue; + } + + if (cursor_col >= max_columns) + { + cursor_col = column; + if (++cursor_row >= max_rows) + { + break; + } + } + + terminal_windows::window_put_char(proc, cursor_col, cursor_row, ch, color); + ++cursor_col; + } +} + +void fill_rect(size_t column, size_t row, size_t width, size_t height, char ch, Color color) +{ + Process *proc = current_process(); + if (!ensure_window_for(proc)) + { + return; + } + + const size_t max_columns = columns(); + const size_t max_rows = rows(); + + for (size_t y = 0; y < height; ++y) + { + size_t target_row = row + y; + if (target_row >= max_rows) + { + break; + } + + for (size_t x = 0; x < width; ++x) + { + size_t target_col = column + x; + if (target_col >= max_columns) + { + break; + } + + terminal_windows::window_put_char(proc, target_col, target_row, ch, color); + } + } +} + +void set_cursor(size_t row, size_t column, bool active) +{ + Process *proc = current_process(); + if (!ensure_window_for(proc)) + { + return; + } + + terminal_windows::window_set_cursor(proc, row, column, active); +} + +bool get_cursor(size_t &row, size_t &column) +{ + row = 0; + column = 0; + + Process *proc = current_process(); + if (!ensure_window_for(proc)) + { + return false; + } + + return terminal_windows::window_get_cursor(proc, row, column); +} + +void present() +{ + Process *proc = current_process(); + if (!ensure_window_for(proc)) + { + return; + } + + terminal_windows::window_present(proc); +} + +} // namespace graphics + diff --git a/src/kernel/gui.cpp b/src/kernel/gui.cpp index a749598..ee02575 100644 --- a/src/kernel/gui.cpp +++ b/src/kernel/gui.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace gui { @@ -26,6 +27,227 @@ constexpr Color BOTTOM_COLOR{10, 14, 22}; bool g_background_override_active = false; uint32_t g_background_override_color = 0; +constexpr uint32_t CURSOR_WIDTH = 16; +constexpr uint32_t CURSOR_HEIGHT = 16; +constexpr uint32_t CURSOR_STRIDE = (CURSOR_WIDTH + 7) / 8; + +constexpr uint8_t CURSOR_OUTLINE[CURSOR_HEIGHT * CURSOR_STRIDE] = { + 0x01, 0x00, // Row 0: .......B........ + 0x01, 0x00, // Row 1: .......B........ + 0x01, 0x00, // Row 2: .......B........ + 0x01, 0x00, // Row 3: .......B........ + 0x01, 0x00, // Row 4: .......B........ + 0x01, 0x00, // Row 5: .......B........ + 0x01, 0x00, // Row 6: .......B........ + 0xFF, 0xFF, // Row 7: BBBBBBBBBBBBBBBB + 0x01, 0x00, // Row 8: .......B........ + 0x01, 0x00, // Row 9: .......B........ + 0x01, 0x00, // Row 10: .......B........ + 0x01, 0x00, // Row 11: .......B........ + 0x01, 0x00, // Row 12: .......B........ + 0x01, 0x00, // Row 13: .......B........ + 0x01, 0x00, // Row 14: .......B........ + 0x01, 0x00, // Row 15: .......B........ +}; + +constexpr uint8_t CURSOR_FILL[CURSOR_HEIGHT * CURSOR_STRIDE] = { + 0x01, 0x00, // Row 0: .......B........ + 0x01, 0x00, // Row 1: .......B........ + 0x01, 0x00, // Row 2: .......B........ + 0x01, 0x00, // Row 3: .......B........ + 0x01, 0x00, // Row 4: .......B........ + 0x01, 0x00, // Row 5: .......B........ + 0x01, 0x00, // Row 6: .......B........ + 0xFF, 0xFF, // Row 7: BBBBBBBBBBBBBBBB + 0x01, 0x00, // Row 8: .......B........ + 0x01, 0x00, // Row 9: .......B........ + 0x01, 0x00, // Row 10: .......B........ + 0x01, 0x00, // Row 11: .......B........ + 0x01, 0x00, // Row 12: .......B........ + 0x01, 0x00, // Row 13: .......B........ + 0x01, 0x00, // Row 14: .......B........ + 0x01, 0x00, // Row 15: .......B........ +}; +struct CursorState +{ + int32_t x = 0; + int32_t y = 0; + uint8_t buttons = 0; + bool visible = false; + bool drawn = false; + uint32_t saved_height = 0; + uint32_t row_width[CURSOR_HEIGHT] = {0}; + uint32_t background[CURSOR_HEIGHT][CURSOR_WIDTH] = {{0}}; +}; + +CursorState g_cursor{}; + +void clamp_cursor() +{ + if (!framebuffer::is_available()) + { + if (g_cursor.x < 0) + { + g_cursor.x = 0; + } + if (g_cursor.y < 0) + { + g_cursor.y = 0; + } + return; + } + + const auto &fb = framebuffer::info(); + if (fb.width == 0 || fb.height == 0) + { + g_cursor.x = 0; + g_cursor.y = 0; + return; + } + + if (g_cursor.x < 0) + { + g_cursor.x = 0; + } + else if (g_cursor.x >= static_cast(fb.width)) + { + g_cursor.x = static_cast(fb.width) - 1; + } + + if (g_cursor.y < 0) + { + g_cursor.y = 0; + } + else if (g_cursor.y >= static_cast(fb.height)) + { + g_cursor.y = static_cast(fb.height) - 1; + } +} + +void capture_cursor_background() +{ + if (!framebuffer::is_available()) + { + return; + } + + const auto &fb = framebuffer::info(); + g_cursor.saved_height = 0; + + for (uint32_t row = 0; row < CURSOR_HEIGHT; ++row) + { + const uint32_t py = static_cast(g_cursor.y + static_cast(row)); + if (py >= fb.height) + { + g_cursor.row_width[row] = 0; + continue; + } + + uint32_t width = 0; + for (uint32_t col = 0; col < CURSOR_WIDTH; ++col) + { + const uint32_t px = static_cast(g_cursor.x + static_cast(col)); + if (px >= fb.width) + { + break; + } + g_cursor.background[row][col] = framebuffer::peek_pixel(px, py); + width = col + 1; + } + g_cursor.row_width[row] = width; + if (width > 0) + { + g_cursor.saved_height = row + 1; + } + } +} + +void restore_cursor_background() +{ + if (!framebuffer::is_available() || !g_cursor.drawn) + { + return; + } + + const auto &fb = framebuffer::info(); + for (uint32_t row = 0; row < g_cursor.saved_height; ++row) + { + const uint32_t width = g_cursor.row_width[row]; + if (width == 0) + { + continue; + } + const uint32_t py = static_cast(g_cursor.y + static_cast(row)); + if (py >= fb.height) + { + continue; + } + for (uint32_t col = 0; col < width; ++col) + { + const uint32_t px = static_cast(g_cursor.x + static_cast(col)); + if (px >= fb.width) + { + break; + } + framebuffer::fill_rect(px, py, 1, 1, g_cursor.background[row][col]); + } + } + g_cursor.saved_height = 0; + g_cursor.drawn = false; +} + +void render_mouse_cursor() +{ + if (!framebuffer::is_available() || !g_cursor.visible) + { + return; + } + + const auto &fb = framebuffer::info(); + if (fb.width == 0 || fb.height == 0) + { + return; + } + + clamp_cursor(); + capture_cursor_background(); + + const uint32_t outline_color = framebuffer::pack_color(16, 20, 28); + const bool button_active = (g_cursor.buttons & (MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT | MOUSE_BUTTON_MIDDLE)) != 0; + const uint32_t fill_color = button_active ? framebuffer::pack_color(198, 220, 255) + : framebuffer::pack_color(248, 248, 255); + + framebuffer::draw_mono_bitmap(static_cast(g_cursor.x), + static_cast(g_cursor.y), + CURSOR_WIDTH, + CURSOR_HEIGHT, + CURSOR_OUTLINE, + CURSOR_STRIDE, + outline_color, + 0, + true); + framebuffer::draw_mono_bitmap(static_cast(g_cursor.x), + static_cast(g_cursor.y), + CURSOR_WIDTH, + CURSOR_HEIGHT, + CURSOR_FILL, + CURSOR_STRIDE, + fill_color, + outline_color, + true); + + g_cursor.drawn = true; +} + +void suspend_mouse_cursor() +{ + if (!g_cursor.drawn) + { + return; + } + restore_cursor_background(); +} + uint32_t gradient_color(uint32_t y) { if (!framebuffer::is_available()) @@ -71,7 +293,9 @@ void draw_boot_screen() { debug("[GUI] draw_boot_screen"); clear_background_fill_override(); + suspend_mouse_cursor(); draw_background_gradient(); + render_mouse_cursor(); } void draw_workspace(Terminal &terminal) @@ -82,8 +306,10 @@ void draw_workspace(Terminal &terminal) return; } + begin_window_redraw(); draw_background_gradient(); terminal_windows::draw_windows(terminal); + end_window_redraw(); } void process_command(const GuiCommand &command, Terminal &terminal, Process *requester) @@ -160,4 +386,64 @@ void clear_background_fill_override() g_background_override_active = false; } +void initialize_mouse_cursor(int32_t x, int32_t y, uint8_t buttons) +{ + if (!framebuffer::is_available()) + { + return; + } + + g_cursor.visible = true; + suspend_mouse_cursor(); + g_cursor.x = x; + g_cursor.y = y; + g_cursor.buttons = buttons; + clamp_cursor(); + render_mouse_cursor(); +} + +void handle_mouse_event(const MouseEvent &event, Terminal &terminal) +{ + if (!framebuffer::is_available()) + { + return; + } + + g_cursor.visible = true; + + const bool position_changed = (event.x != g_cursor.x) || (event.y != g_cursor.y); + const bool buttons_changed = (event.buttons != g_cursor.buttons); + const bool cursor_changed = position_changed || buttons_changed; + + if (cursor_changed) + { + suspend_mouse_cursor(); + g_cursor.x = event.x; + g_cursor.y = event.y; + g_cursor.buttons = event.buttons; + clamp_cursor(); + } + + const bool consumed = terminal_windows::handle_mouse_event(terminal, event); + (void)consumed; + + if (g_cursor.visible && !g_cursor.drawn) + { + render_mouse_cursor(); + } +} + +void begin_window_redraw() +{ + suspend_mouse_cursor(); +} + +void end_window_redraw() +{ + if (g_cursor.visible && !g_cursor.drawn) + { + render_mouse_cursor(); + } +} + } // namespace gui diff --git a/src/kernel/mouse.cpp b/src/kernel/mouse.cpp index 282eab7..5e0129b 100644 --- a/src/kernel/mouse.cpp +++ b/src/kernel/mouse.cpp @@ -1,6 +1,384 @@ #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +extern Terminal terminal; + +namespace +{ +constexpr uint16_t PS2_CMD_PORT = 0x64; +constexpr uint16_t PS2_DATA_PORT = 0x60; + +constexpr uint8_t PS2_STATUS_OUTPUT_FULL = 0x01u; +constexpr uint8_t PS2_STATUS_INPUT_FULL = 0x02u; +constexpr uint8_t PS2_STATUS_MOUSE_DATA = 0x20u; + +constexpr uint8_t PS2_ENABLE_AUX_DEVICE = 0xA8u; +constexpr uint8_t PS2_COMMAND_GET_STATUS = 0x20u; +constexpr uint8_t PS2_COMMAND_SET_STATUS = 0x60u; + +constexpr uint8_t PS2_MOUSE_SET_DEFAULTS = 0xF6u; +constexpr uint8_t PS2_MOUSE_ENABLE_PACKET_STREAMING = 0xF4u; +constexpr uint8_t PS2_MOUSE_SET_SAMPLE_RATE = 0xF3u; +constexpr uint8_t PS2_MOUSE_GET_DEVICE_ID = 0xF2u; + +constexpr uint8_t PS2_MOUSE_ACK = 0xFAu; + +constexpr uint8_t IRQ_MOUSE = 12; +constexpr uint8_t ISR_MOUSE = static_cast(32 + IRQ_MOUSE); + +constexpr size_t MAX_PACKET_SIZE = 4; + +volatile int32_t g_mouse_x = 0; +volatile int32_t g_mouse_y = 0; +volatile uint8_t g_mouse_buttons = 0; +volatile uint8_t g_mouse_available = 0; + +MouseState g_state{}; + +uint8_t g_packet[MAX_PACKET_SIZE] = {}; +uint8_t g_packet_index = 0; +uint8_t g_bytes_expected = 3; +bool g_has_scroll_wheel = false; + +void mouse_wait(uint8_t type) +{ + // type 0 = wait for data, type 1 = wait for input clear + int timeout = 100000; + if (type == 0) + { + while (timeout-- > 0) + { + if (inb(PS2_CMD_PORT) & PS2_STATUS_OUTPUT_FULL) + { + return; + } + } + } + else + { + while (timeout-- > 0) + { + if ((inb(PS2_CMD_PORT) & PS2_STATUS_INPUT_FULL) == 0) + { + return; + } + } + } +} + +void mouse_flush_output() +{ + for (int i = 0; i < 256; ++i) + { + if ((inb(PS2_CMD_PORT) & PS2_STATUS_OUTPUT_FULL) == 0) + { + break; + } + (void)inb(PS2_DATA_PORT); + } +} + +void mouse_write(uint8_t data) +{ + mouse_wait(1); + outb(PS2_CMD_PORT, 0xD4); + mouse_wait(1); + outb(PS2_DATA_PORT, data); +} + +uint8_t mouse_read() +{ + mouse_wait(0); + return inb(PS2_DATA_PORT); +} + +bool mouse_send_command(uint8_t command) +{ + mouse_write(command); + uint8_t response = mouse_read(); + return response == PS2_MOUSE_ACK; +} + +bool mouse_send_command(uint8_t command, uint8_t value) +{ + if (!mouse_send_command(command)) + { + return false; + } + mouse_write(value); + return mouse_read() == PS2_MOUSE_ACK; +} + +void clamp_position() +{ + if (!framebuffer::is_available()) + { + if (g_mouse_x < 0) + { + g_mouse_x = 0; + } + if (g_mouse_y < 0) + { + g_mouse_y = 0; + } + return; + } + + const auto &fb = framebuffer::info(); + if (fb.width == 0 || fb.height == 0) + { + g_mouse_x = 0; + g_mouse_y = 0; + return; + } + + if (g_mouse_x < 0) + { + g_mouse_x = 0; + } + else if (g_mouse_x >= static_cast(fb.width)) + { + g_mouse_x = static_cast(fb.width) - 1; + } + + if (g_mouse_y < 0) + { + g_mouse_y = 0; + } + else if (g_mouse_y >= static_cast(fb.height)) + { + g_mouse_y = static_cast(fb.height) - 1; + } +} + +void dispatch_event(MouseEvent &event) +{ + Process *target = scheduler_get_foreground(); + if (target == nullptr) + { + target = shell_get_process(); + } + if (target == nullptr) + { + target = scheduler_current_process(); + } + + if (target == nullptr) + { + return; + } + + event.target_pid = target->pid; + + IOEvent io_event{}; + io_event.type = EVENT_MOUSE; + io_event.data.mouse = event; + push_io_event(target, io_event); + scheduler_resume_processes_for_event(HookType::SIGNAL, static_cast(target->pid)); +} + +void handle_packet() +{ + const uint8_t status = g_packet[0]; + const bool x_overflow = (status & 0x40u) != 0; + const bool y_overflow = (status & 0x80u) != 0; + if (x_overflow || y_overflow) + { + g_packet_index = 0; + return; + } + + const int16_t dx = static_cast(g_packet[1]); + const int16_t dy_raw = static_cast(g_packet[2]); + const int16_t dy = static_cast(-dy_raw); + + const int32_t previous_x = g_mouse_x; + const int32_t previous_y = g_mouse_y; + + g_mouse_x += dx; + g_mouse_y += dy; + clamp_position(); + + const uint8_t buttons = static_cast(status & 0x07u); + const uint8_t changed = static_cast(buttons ^ g_mouse_buttons); + + g_mouse_buttons = buttons; + + int8_t scroll_x = 0; + int8_t scroll_y = 0; + if (g_has_scroll_wheel && g_bytes_expected == 4) + { + scroll_y = static_cast(g_packet[3]); + } + + g_state.x = g_mouse_x; + g_state.y = g_mouse_y; + g_state.buttons = g_mouse_buttons; + g_state.available = g_mouse_available; + + MouseEvent event{}; + event.x = g_mouse_x; + event.y = g_mouse_y; + event.dx = static_cast(g_mouse_x - previous_x); + event.dy = static_cast(g_mouse_y - previous_y); + event.scroll_x = scroll_x; + event.scroll_y = scroll_y; + event.buttons = buttons; + event.changed = changed; + event.target_pid = -1; + + gui::handle_mouse_event(event, terminal); + dispatch_event(event); +} + +void mouse_callback(registers_t *regs) +{ + (void)regs; + + uint8_t status = inb(PS2_CMD_PORT); + while (status & PS2_STATUS_OUTPUT_FULL) + { + if ((status & PS2_STATUS_MOUSE_DATA) == 0) + { + break; + } + + uint8_t data = inb(PS2_DATA_PORT); + + if (g_packet_index == 0 && (data & 0x08u) == 0) + { + status = inb(PS2_CMD_PORT); + continue; + } + + g_packet[g_packet_index++] = data; + if (g_packet_index >= g_bytes_expected) + { + handle_packet(); + g_packet_index = 0; + } + + status = inb(PS2_CMD_PORT); + } + + pic_send_eoi(IRQ_MOUSE); +} + +bool try_enable_scroll_wheel() +{ + bool success = mouse_send_command(PS2_MOUSE_SET_SAMPLE_RATE, 200) && + mouse_send_command(PS2_MOUSE_SET_SAMPLE_RATE, 100) && + mouse_send_command(PS2_MOUSE_SET_SAMPLE_RATE, 80); + + if (!success) + { + return false; + } + + mouse_write(PS2_MOUSE_GET_DEVICE_ID); + if (mouse_read() != PS2_MOUSE_ACK) + { + return false; + } + const uint8_t id = mouse_read(); + return id == 0x03u || id == 0x04u; +} +} // namespace + void mouse_initialize() { - // GUI reset: mouse support will be rebuilt incrementally. + debug("[MOUSE] Initializing PS/2 mouse"); + + g_packet_index = 0; + g_bytes_expected = 3; + g_has_scroll_wheel = false; + g_mouse_buttons = 0; + + mouse_flush_output(); + + mouse_wait(1); + outb(PS2_CMD_PORT, PS2_ENABLE_AUX_DEVICE); + + mouse_wait(1); + outb(PS2_CMD_PORT, PS2_COMMAND_GET_STATUS); + mouse_wait(0); + uint8_t status = inb(PS2_DATA_PORT); + status |= 0x02u; // Enable IRQ12 + status |= 0x20u; // Enable mouse clock + + mouse_wait(1); + outb(PS2_CMD_PORT, PS2_COMMAND_SET_STATUS); + mouse_wait(1); + outb(PS2_DATA_PORT, status); + + if (!mouse_send_command(PS2_MOUSE_SET_DEFAULTS)) + { + error("[MOUSE] Failed to set defaults"); + } + + g_has_scroll_wheel = try_enable_scroll_wheel(); + g_bytes_expected = g_has_scroll_wheel ? 4 : 3; + + if (!mouse_send_command(PS2_MOUSE_SET_DEFAULTS)) + { + error("[MOUSE] Failed to reset defaults after ID probe"); + } + + if (!mouse_send_command(PS2_MOUSE_ENABLE_PACKET_STREAMING)) + { + error("[MOUSE] Failed to enable streaming"); + } + + register_interrupt_handler(ISR_MOUSE, mouse_callback); + pic_unmask_irq(IRQ_MOUSE); + + g_mouse_available = framebuffer::is_available() ? 1 : 0; + + if (framebuffer::is_available()) + { + const auto &fb = framebuffer::info(); + g_mouse_x = static_cast(fb.width / 2); + g_mouse_y = static_cast(fb.height / 2); + } + else + { + g_mouse_x = 0; + g_mouse_y = 0; + } + + clamp_position(); + + g_state.x = g_mouse_x; + g_state.y = g_mouse_y; + g_state.buttons = g_mouse_buttons; + g_state.available = g_mouse_available; + + if (g_mouse_available) + { + gui::initialize_mouse_cursor(g_mouse_x, g_mouse_y, g_mouse_buttons); + } + + debug("[MOUSE] Initialized (wheel=%u, packet_bytes=%u, pos=%d,%d)", + g_has_scroll_wheel ? 1u : 0u, + static_cast(g_bytes_expected), + static_cast(g_mouse_x), + static_cast(g_mouse_y)); +} + +MouseState mouse_get_state() +{ + return g_state; } diff --git a/src/kernel/shell.cpp b/src/kernel/shell.cpp index f0db2dc..fb3f721 100644 --- a/src/kernel/shell.cpp +++ b/src/kernel/shell.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include extern Terminal terminal; extern shell_command_t commands[]; @@ -27,13 +29,20 @@ static Process* g_shell_process = nullptr; typedef struct { char buffer[SHELL_BUFFER_SIZE]; - size_t index; + size_t cursor; + size_t length; bool input_enabled; bool prompt_visible; char history[SHELL_HISTORY_SIZE][SHELL_BUFFER_SIZE]; size_t history_count; int history_nav; size_t prompt_length; + size_t prompt_row; + size_t prompt_col; + size_t cursor_row; + size_t cursor_col; + size_t rendered_chars; + char prompt_cache[VFS_MAX_PATH + 16]; } ShellState; static ShellState shell; @@ -44,13 +53,102 @@ static inline void safe_strncpy(char* dst, const char* src, size_t n) { dst[n - 1] = '\0'; } -// Clear `length` characters from the terminal line +// Legacy line clearing helper used when framebuffer GUI is unavailable static void clear_line(size_t length) { for (size_t i = 0; i < length; ++i) printf("\b"); for (size_t i = 0; i < length; ++i) printf(" "); for (size_t i = 0; i < length; ++i) printf("\b"); } +static inline size_t shell_window_width() { + return framebuffer::is_available() ? graphics::columns() : Terminal::VGA_WIDTH; +} + +static inline size_t shell_window_height() { + return framebuffer::is_available() ? graphics::rows() : Terminal::VGA_HEIGHT; +} + +static void shell_advance_position(size_t &row, size_t &col) { + ++col; + if (col >= shell_window_width()) { + col = 0; + if (row + 1 < shell_window_height()) { + ++row; + } + } +} + +static void shell_compute_position_from_offset(size_t offset, size_t &row, size_t &col) { + row = shell.prompt_row; + col = shell.prompt_col; + for (size_t i = 0; i < offset; ++i) { + shell_advance_position(row, col); + } +} + +static void shell_render_input() { + if (!shell.prompt_visible || g_shell_process == nullptr || !framebuffer::is_available()) { + return; + } + + const size_t height = shell_window_height(); + const size_t width = shell_window_width(); + const graphics::Color color = graphics::make_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK); + + shell.buffer[shell.length] = '\0'; + + size_t row = shell.prompt_row; + size_t col = shell.prompt_col; + + auto put_char = [&](char ch) { + if (row < height && col < width) { + graphics::put_char(col, row, ch, color); + } + shell_advance_position(row, col); + }; + + for (size_t i = 0; i < shell.prompt_length; ++i) { + put_char(shell.prompt_cache[i]); + } + + for (size_t i = 0; i < shell.length; ++i) { + put_char(shell.buffer[i]); + } + + size_t total = shell.prompt_length + shell.length; + if (shell.rendered_chars > total) { + size_t diff = shell.rendered_chars - total; + while (diff-- > 0) { + put_char(' '); + } + } + shell.rendered_chars = total; + + size_t caret_row; + size_t caret_col; + shell_compute_position_from_offset(shell.prompt_length + shell.cursor, caret_row, caret_col); + shell.cursor_row = caret_row; + shell.cursor_col = caret_col; + graphics::set_cursor(caret_row, caret_col, true); + graphics::present(); +} + +static void shell_render_input_legacy() { + size_t previous_total = shell.rendered_chars; + if (previous_total > 0) { + clear_line(previous_total); + } + printf("%s", shell.prompt_cache); + printf("%s", shell.buffer); + size_t tail = (shell.length > shell.cursor) ? (shell.length - shell.cursor) : 0; + for (size_t i = 0; i < tail; ++i) { + printf("\b"); + } + shell.rendered_chars = shell.prompt_length + shell.length; + shell.cursor_row = 0; + shell.cursor_col = shell.prompt_length + shell.cursor; +} + // Print the shell prompt static void shell_print_prompt(void) { const char* cwd = vfs_getcwd(); @@ -80,19 +178,50 @@ static void shell_print_prompt(void) { prompt[pos] = '\0'; - printf("%s", prompt); shell.prompt_length = pos; - shell.prompt_visible = true; + memcpy(shell.prompt_cache, prompt, pos); + shell.prompt_cache[pos] = '\0'; + + shell.cursor = 0; + shell.length = 0; + shell.buffer[0] = '\0'; + shell.rendered_chars = 0; + + if (g_shell_process != nullptr && framebuffer::is_available()) { + graphics::ensure_window(); + size_t start_row = 0; + size_t start_col = 0; + graphics::get_cursor(start_row, start_col); + shell.prompt_row = start_row; + shell.prompt_col = start_col; + shell.prompt_visible = true; + shell_render_input(); + } else { + printf("%s", prompt); + shell.prompt_row = 0; + shell.prompt_col = shell.prompt_length; + shell.rendered_chars = shell.prompt_length; + shell.cursor_row = 0; + shell.cursor_col = shell.prompt_col; + shell.prompt_visible = true; + } } // Initialize shell state and print welcome message void shell_init(void) { - shell.index = 0; + shell.cursor = 0; + shell.length = 0; + shell.rendered_chars = 0; + shell.prompt_row = 0; + shell.prompt_col = 0; + shell.cursor_row = 0; + shell.cursor_col = 0; shell.input_enabled = true; shell.prompt_visible = false; shell.history_count = 0; shell.history_nav = -1; shell.prompt_length = 0; + shell.buffer[0] = '\0'; printf("Welcome to nutshell!\n"); shell_print_prompt(); } @@ -138,6 +267,16 @@ void shell_set_input_enabled(bool enabled) { shell.input_enabled = enabled; if (!enabled) { shell.prompt_visible = false; + if (g_shell_process != nullptr && framebuffer::is_available()) { + graphics::set_cursor(shell.cursor_row, shell.cursor_col, false); + } + } else { + shell.prompt_visible = true; + if (framebuffer::is_available() && g_shell_process != nullptr) { + shell_render_input(); + } else { + shell_render_input_legacy(); + } } } @@ -169,16 +308,22 @@ void shell_handle_key(keyboard_event ke) { } if (!shell.input_enabled) return; + const bool graphics = framebuffer::is_available() && g_shell_process != nullptr; + // Up arrow: previous history if (ke.up_arrow) { const char* prev = shell_history_prev(); if (prev) { - size_t len = shell.prompt_length + shell.index; - clear_line(len); + size_t previous_total = shell.prompt_length + shell.length; safe_strncpy(shell.buffer, prev, SHELL_BUFFER_SIZE); - shell.index = strlen(shell.buffer); - shell_print_prompt(); - printf("%s", shell.buffer); + shell.length = strlen(shell.buffer); + shell.cursor = shell.length; + if (graphics) { + shell_render_input(); + } else { + shell.rendered_chars = previous_total; + shell_render_input_legacy(); + } } return; } @@ -186,37 +331,80 @@ void shell_handle_key(keyboard_event ke) { // Down arrow: next history if (ke.down_arrow) { const char* next = shell_history_next(); - size_t len = shell.prompt_length + shell.index; - clear_line(len); + size_t previous_total = shell.prompt_length + shell.length; if (next && *next) { safe_strncpy(shell.buffer, next, SHELL_BUFFER_SIZE); - shell.index = strlen(shell.buffer); - shell_print_prompt(); - printf("%s", shell.buffer); + shell.length = strlen(shell.buffer); + shell.cursor = shell.length; } else { shell.buffer[0] = '\0'; - shell.index = 0; - shell_print_prompt(); + shell.length = 0; + shell.cursor = 0; + } + if (graphics) { + shell_render_input(); + } else { + shell.rendered_chars = previous_total; + shell_render_input_legacy(); + } + return; + } + + if (ke.left_arrow) { + if (shell.cursor > 0) { + shell.cursor--; + if (graphics) { + shell_render_input(); + } else { + shell_render_input_legacy(); + } + } + return; + } + + if (ke.right_arrow) { + if (shell.cursor < shell.length) { + shell.cursor++; + if (graphics) { + shell_render_input(); + } else { + shell_render_input_legacy(); + } } return; } // Backspace if (ke.backspace) { - if (shell.index > 0) { - shell.index--; - printf("\b \b"); + if (shell.cursor > 0) { + size_t previous_total = shell.prompt_length + shell.length; + memmove(&shell.buffer[shell.cursor - 1], + &shell.buffer[shell.cursor], + shell.length - shell.cursor + 1); + shell.cursor--; + if (shell.length > 0) { + shell.length--; + } + if (graphics) { + shell_render_input(); + } else { + shell.rendered_chars = previous_total; + shell_render_input_legacy(); + } } return; } // Enter: execute command if (ke.enter) { - shell.buffer[shell.index] = '\0'; + shell.buffer[shell.length] = '\0'; printf("\n"); shell_history_add(shell.buffer); shell_process_command(shell.buffer); - shell.index = 0; + shell.cursor = 0; + shell.length = 0; + shell.buffer[0] = '\0'; + shell.rendered_chars = 0; shell_history_reset(); shell.prompt_visible = false; // Only print a new prompt if input is still enabled @@ -228,9 +416,20 @@ void shell_handle_key(keyboard_event ke) { // Printable character char c = kb_to_ascii(ke); - if (c && shell.index < SHELL_BUFFER_SIZE - 1) { - shell.buffer[shell.index++] = c; - printf("%c", c); + if (c && shell.length < SHELL_BUFFER_SIZE - 1) { + size_t previous_total = shell.prompt_length + shell.length; + memmove(&shell.buffer[shell.cursor + 1], + &shell.buffer[shell.cursor], + shell.length - shell.cursor + 1); + shell.buffer[shell.cursor] = c; + shell.cursor++; + shell.length++; + if (graphics) { + shell_render_input(); + } else { + shell.rendered_chars = previous_total; + shell_render_input_legacy(); + } } } diff --git a/src/kernel/terminal_windows.cpp b/src/kernel/terminal_windows.cpp index 7c8003b..87120e9 100644 --- a/src/kernel/terminal_windows.cpp +++ b/src/kernel/terminal_windows.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -14,6 +15,13 @@ namespace terminal_windows { namespace { +struct DirtyRegion +{ + size_t min_row; + size_t max_row; + bool full_refresh; +}; + struct Window { bool in_use = false; @@ -21,6 +29,7 @@ struct Window Terminal::Snapshot snapshot{}; uint32_t frame_x = 0; uint32_t frame_y = 0; + DirtyRegion dirty{}; }; struct RGB @@ -40,6 +49,17 @@ constexpr uint32_t INITIAL_FRAME_Y = 72; constexpr uint32_t CASCADE_STEP_X = 28; constexpr uint32_t CASCADE_STEP_Y = 28; +constexpr uint32_t CLOSE_BUTTON_SIZE = 14; +constexpr uint32_t CLOSE_BUTTON_MARGIN = 8; + +constexpr RGB CLOSE_BUTTON_BG{200, 60, 70}; +constexpr RGB CLOSE_BUTTON_FG{240, 240, 240}; +constexpr RGB FRAME_BORDER_HIGHLIGHT{38, 44, 58}; +constexpr RGB TITLE_TOP_HIGHLIGHT{130, 150, 210}; +constexpr RGB TITLE_BOTTOM_SHADOW{18, 20, 30}; +constexpr RGB ACTIVE_ACCENT_GLOW{100, 160, 255}; +constexpr RGB TITLE_TEXT_SHADOW{18, 24, 32}; + constexpr RGB FRAME_BORDER_COLOR{18, 22, 30}; constexpr RGB FRAME_BACKGROUND_COLOR{30, 34, 46}; constexpr RGB TITLE_ACTIVE_TOP{82, 128, 204}; @@ -69,16 +89,12 @@ constexpr RGB VGA_PALETTE[16] = { }; constexpr char FALLBACK_GLYPH = '?'; -struct DirtyRegion -{ - size_t min_row; - size_t max_row; - bool full_refresh; -}; - Window g_windows[MAX_WINDOWS]; size_t g_window_count = 0; -int g_active_index = -1; +int g_active_slot = -1; + +size_t g_z_order[MAX_WINDOWS]; +size_t g_z_count = 0; uint32_t g_content_width = 0; uint32_t g_content_height = 0; @@ -94,11 +110,154 @@ bool g_blank_ready = false; bool g_logged_sample_pixel = false; #endif +struct WindowHitResult +{ + int slot = -1; + bool on_title_bar = false; + bool on_close_button = false; + uint32_t local_x = 0; + uint32_t local_y = 0; +}; + +bool g_dragging_window = false; +int g_drag_slot = -1; +int32_t g_drag_offset_x = 0; +int32_t g_drag_offset_y = 0; + +constexpr int INVALID_SLOT = -1; + +bool window_slot_valid(size_t slot) +{ + return slot < MAX_WINDOWS && g_windows[slot].in_use; +} + +size_t find_stack_position(size_t slot) +{ + for (size_t i = 0; i < g_z_count; ++i) + { + if (g_z_order[i] == slot) + { + return i; + } + } + return MAX_WINDOWS; +} + +DirtyRegion make_empty_dirty_region(); +void reset_dirty(DirtyRegion ®ion); + +void push_slot_to_top(size_t slot) +{ + if (slot >= MAX_WINDOWS) + { + return; + } + + size_t position = find_stack_position(slot); + if (position == MAX_WINDOWS) + { + if (g_z_count < MAX_WINDOWS) + { + g_z_order[g_z_count++] = slot; + } + return; + } + + for (size_t i = position; i + 1 < g_z_count; ++i) + { + g_z_order[i] = g_z_order[i + 1]; + } + g_z_order[g_z_count - 1] = slot; +} + +void remove_slot_from_stack(size_t slot) +{ + size_t position = find_stack_position(slot); + if (position == MAX_WINDOWS) + { + return; + } + + for (size_t i = position; i + 1 < g_z_count; ++i) + { + g_z_order[i] = g_z_order[i + 1]; + } + if (g_z_count > 0) + { + --g_z_count; + } +} + +size_t allocate_window_slot() +{ + for (size_t i = 0; i < MAX_WINDOWS; ++i) + { + if (!g_windows[i].in_use) + { + g_windows[i] = Window{}; + g_windows[i].in_use = true; + reset_dirty(g_windows[i].dirty); + ++g_window_count; + return i; + } + } + return MAX_WINDOWS; +} + +void release_window_slot(size_t slot) +{ + if (!window_slot_valid(slot)) + { + return; + } + g_windows[slot] = Window{}; + g_windows[slot].in_use = false; + reset_dirty(g_windows[slot].dirty); + if (g_window_count > 0) + { + --g_window_count; + } +} + DirtyRegion make_empty_dirty_region() { return DirtyRegion{Terminal::VGA_HEIGHT, 0, false}; } +void reset_dirty(DirtyRegion ®ion) +{ + region = make_empty_dirty_region(); +} + +void stop_dragging(); + +void close_window_slot(size_t slot, Terminal &terminal) +{ + if (!window_slot_valid(slot)) + { + return; + } + + if (g_dragging_window && g_drag_slot == static_cast(slot)) + { + stop_dragging(); + } + + remove_slot_from_stack(slot); + release_window_slot(slot); + + if (g_z_count == 0) + { + g_active_slot = INVALID_SLOT; + } + else + { + g_active_slot = static_cast(g_z_order[g_z_count - 1]); + } + + gui::draw_workspace(terminal); +} + bool dirty_region_has_updates(const DirtyRegion ®ion) { return region.full_refresh || region.min_row <= region.max_row; @@ -140,6 +299,96 @@ uint32_t pack(const RGB &color) return framebuffer::pack_color(color.r, color.g, color.b); } +void stop_dragging() +{ + g_dragging_window = false; + g_drag_slot = INVALID_SLOT; + g_drag_offset_x = 0; + g_drag_offset_y = 0; +} + +WindowHitResult hit_test_window(uint32_t x, uint32_t y) +{ + WindowHitResult result{}; + if (!framebuffer::is_available() || !g_geometry_ready || g_z_count == 0) + { + return result; + } + + auto test_slot = [&](size_t slot) -> bool { + if (!window_slot_valid(slot)) + { + return false; + } + + const Window &window = g_windows[slot]; + + const uint32_t frame_x = window.frame_x; + const uint32_t frame_y = window.frame_y; + + if (x < frame_x || y < frame_y) + { + return false; + } + + const uint32_t rel_x = x - frame_x; + const uint32_t rel_y = y - frame_y; + + if (rel_x >= g_frame_width || rel_y >= g_frame_height) + { + return false; + } + + const uint32_t inner_width = g_frame_width > 2U * FRAME_BORDER ? g_frame_width - 2U * FRAME_BORDER : 0U; + const uint32_t close_x_start = FRAME_BORDER + (inner_width > CLOSE_BUTTON_SIZE + CLOSE_BUTTON_MARGIN + ? inner_width - CLOSE_BUTTON_MARGIN - CLOSE_BUTTON_SIZE + : inner_width); + const uint32_t close_y_start = FRAME_BORDER + (TITLE_BAR_HEIGHT > CLOSE_BUTTON_SIZE + ? (TITLE_BAR_HEIGHT - CLOSE_BUTTON_SIZE) / 2U + : 0U); + if (inner_width > CLOSE_BUTTON_SIZE + CLOSE_BUTTON_MARGIN && + rel_x >= close_x_start && rel_x < close_x_start + CLOSE_BUTTON_SIZE && + rel_y >= close_y_start && rel_y < close_y_start + CLOSE_BUTTON_SIZE) + { + result.slot = static_cast(slot); + result.on_close_button = true; + result.on_title_bar = false; + result.local_x = rel_x; + result.local_y = rel_y; + return true; + } + + const uint32_t title_region = FRAME_BORDER + TITLE_BAR_HEIGHT; + + result.slot = static_cast(slot); + result.on_close_button = false; + result.on_title_bar = (rel_y < title_region); + result.local_x = rel_x; + result.local_y = rel_y; + return true; + }; + + if (g_active_slot >= 0 && test_slot(static_cast(g_active_slot))) + { + return result; + } + + for (size_t i = g_z_count; i > 0; --i) + { + size_t slot = g_z_order[i - 1]; + if (static_cast(slot) == g_active_slot) + { + continue; + } + if (test_slot(slot)) + { + return result; + } + } + + return result; +} + uint32_t vga_to_rgb(uint8_t index) { index &= 0x0F; @@ -149,6 +398,8 @@ uint32_t vga_to_rgb(uint8_t index) uint32_t content_origin_x(const Window &window); uint32_t content_origin_y(const Window &window); +WindowHitResult hit_test_window(uint32_t x, uint32_t y); +void stop_dragging(); void ensure_geometry(Terminal &terminal) { @@ -413,6 +664,38 @@ void draw_snapshot_contents(const Window &window, bool active, size_t start_row } } +void present_window_slot(size_t slot) +{ + if (!window_slot_valid(slot)) + { + return; + } + + Window &window = g_windows[slot]; + if (!dirty_region_has_updates(window.dirty)) + { + return; + } + + if (g_active_slot != static_cast(slot)) + { + reset_dirty(window.dirty); + return; + } + + gui::begin_window_redraw(); + if (window.dirty.full_refresh) + { + draw_snapshot_contents(window, true); + } + else + { + draw_snapshot_contents(window, true, window.dirty.min_row, window.dirty.max_row); + } + gui::end_window_redraw(); + reset_dirty(window.dirty); +} + void clamp_frame(uint32_t &frame_x, uint32_t &frame_y) { if (!framebuffer::is_available() || !g_geometry_ready) @@ -460,9 +743,9 @@ int find_window_index(Process *proc) return -1; } - for (size_t i = 0; i < g_window_count; ++i) + for (size_t i = 0; i < MAX_WINDOWS; ++i) { - if (g_windows[i].in_use && g_windows[i].owner == proc) + if (window_slot_valid(i) && g_windows[i].owner == proc) { return static_cast(i); } @@ -470,7 +753,7 @@ int find_window_index(Process *proc) return -1; } -void draw_text(uint32_t x, uint32_t y, const char *text, uint32_t color, uint32_t max_width) +void draw_text(uint32_t x, uint32_t y, const char *text, uint32_t color, uint32_t max_width, bool drop_shadow = false) { if (text == nullptr) { @@ -495,6 +778,18 @@ void draw_text(uint32_t x, uint32_t y, const char *text, uint32_t color, uint32_ for (size_t i = 0; i < length; ++i) { const uint8_t *glyph = gui::glyph_for(buffer[i]); + if (drop_shadow) + { + framebuffer::draw_mono_bitmap(x + 1, + y + 1, + gui::FONT_WIDTH, + gui::FONT_HEIGHT, + glyph, + 1, + pack(TITLE_TEXT_SHADOW), + 0, + true); + } framebuffer::draw_mono_bitmap(x, y, gui::FONT_WIDTH, @@ -523,6 +818,7 @@ void draw_window_frame(const Window &window, bool active) const uint32_t inner_height = g_frame_height > 2U * FRAME_BORDER ? g_frame_height - 2U * FRAME_BORDER : 0; framebuffer::fill_rect(frame_x, frame_y, g_frame_width, g_frame_height, pack(FRAME_BORDER_COLOR)); + framebuffer::fill_rect(frame_x, frame_y, g_frame_width, 1, pack(FRAME_BORDER_HIGHLIGHT)); if (inner_width == 0 || inner_height == 0) { @@ -548,7 +844,61 @@ void draw_window_frame(const Window &window, bool active) framebuffer::fill_rect(inner_x, title_y + row, inner_width, 1, framebuffer::pack_color(r, g, b)); } + framebuffer::fill_rect(inner_x, title_y, inner_width, 1, pack(TITLE_TOP_HIGHLIGHT)); framebuffer::fill_rect(inner_x, title_y + title_height - 1, inner_width, 1, pack(TITLE_BOTTOM_LINE)); + framebuffer::fill_rect(inner_x, title_y + title_height, inner_width, 1, pack(TITLE_BOTTOM_SHADOW)); + } + + if (active) + { + const uint32_t accent_height = title_height > 4 ? title_height - 4 : title_height; + if (accent_height > 0) + { + framebuffer::fill_rect(frame_x + FRAME_BORDER, + frame_y + FRAME_BORDER + 2, + 2, + accent_height, + pack(ACTIVE_ACCENT_GLOW)); + } + } + + if (inner_width > CLOSE_BUTTON_SIZE + CLOSE_BUTTON_MARGIN) + { + const uint32_t close_x = inner_x + inner_width - CLOSE_BUTTON_MARGIN - CLOSE_BUTTON_SIZE; + const uint32_t close_y = title_y + (title_height > CLOSE_BUTTON_SIZE ? (title_height - CLOSE_BUTTON_SIZE) / 2 : 0); + for (uint32_t row = 0; row < CLOSE_BUTTON_SIZE; ++row) + { + const float t = CLOSE_BUTTON_SIZE > 1 ? static_cast(row) / static_cast(CLOSE_BUTTON_SIZE - 1U) : 0.0f; + const uint8_t r = static_cast(CLOSE_BUTTON_BG.r * (1.0f - t) + 120.0f * t); + const uint8_t g = static_cast(CLOSE_BUTTON_BG.g * (1.0f - t) + 30.0f * t); + const uint8_t b = static_cast(CLOSE_BUTTON_BG.b * (1.0f - t) + 40.0f * t); + framebuffer::fill_rect(close_x, + close_y + row, + CLOSE_BUTTON_SIZE, + 1, + framebuffer::pack_color(r, g, b)); + } + + framebuffer::fill_rect(close_x, + close_y, + CLOSE_BUTTON_SIZE, + 1, + pack(TITLE_TOP_HIGHLIGHT)); + + const uint32_t inset = CLOSE_BUTTON_SIZE / 4; + const uint32_t button_size = CLOSE_BUTTON_SIZE - inset * 2; + const uint32_t button_x = close_x + inset; + const uint32_t button_y = close_y + inset; + + for (uint32_t i = 0; i < button_size; ++i) + { + const uint32_t x0 = button_x + i; + const uint32_t y0 = button_y + i; + framebuffer::fill_rect(x0, y0, 2, 1, pack(CLOSE_BUTTON_FG)); + + const uint32_t x1 = button_x + (button_size - 1 - i); + framebuffer::fill_rect(x1, y0, 2, 1, pack(CLOSE_BUTTON_FG)); + } } const char *title = "Terminal"; @@ -571,7 +921,7 @@ void draw_window_frame(const Window &window, bool active) text_y += (title_height - gui::FONT_HEIGHT) / 2; } - draw_text(text_x, text_y, title, pack(TITLE_TEXT_COLOR), inner_width - 2U * text_margin); + draw_text(text_x, text_y, title, pack(TITLE_TEXT_COLOR), inner_width - 2U * text_margin, true); } void render_window(Terminal &terminal, Window &window, bool active) @@ -581,17 +931,16 @@ void render_window(Terminal &terminal, Window &window, bool active) framebuffer::fill_rect(content_origin_x(window), content_origin_y(window), g_content_width, g_content_height, pack(CONTENT_BACKGROUND_COLOR)); draw_window_frame(window, active); draw_snapshot_contents(window, active); + reset_dirty(window.dirty); } -void focus_window(size_t index, Terminal &terminal) +void focus_window(size_t slot, Terminal &terminal) { - if (index >= g_window_count) + if (!window_slot_valid(slot)) { return; } - const int previous_active = g_active_index; - if (!g_geometry_ready) { ensure_geometry(terminal); @@ -601,19 +950,11 @@ void focus_window(size_t index, Terminal &terminal) } } - if (index != g_window_count - 1) - { - Window selected = g_windows[index]; - for (size_t i = index; i + 1 < g_window_count; ++i) - { - g_windows[i] = g_windows[i + 1]; - } - g_windows[g_window_count - 1] = selected; - index = g_window_count - 1; - } + const int previous_active = g_active_slot; + push_slot_to_top(slot); + g_active_slot = static_cast(slot); - g_active_index = static_cast(index); - if (previous_active != g_active_index) + if (previous_active != g_active_slot) { draw_windows(terminal); } @@ -624,9 +965,13 @@ void reset_windows() for (size_t i = 0; i < MAX_WINDOWS; ++i) { g_windows[i] = Window{}; + reset_dirty(g_windows[i].dirty); + g_z_order[i] = 0; } g_window_count = 0; - g_active_index = -1; + g_z_count = 0; + g_active_slot = INVALID_SLOT; + stop_dragging(); } } // namespace @@ -644,27 +989,35 @@ void init(Terminal &terminal, Process *initial_proc) void draw_windows(Terminal &terminal) { - debug("[GUI] draw_windows count=%u active=%d", static_cast(g_window_count), g_active_index); - if (!framebuffer::is_available() || g_window_count == 0) + gui::begin_window_redraw(); + + debug("[GUI] draw_windows count=%u active_slot=%d", + static_cast(g_window_count), + g_active_slot); + if (!framebuffer::is_available() || g_z_count == 0) { + gui::end_window_redraw(); return; } ensure_geometry(terminal); - for (size_t i = 0; i < g_window_count; ++i) + for (size_t i = 0; i < g_z_count; ++i) { - if (static_cast(i) == g_active_index) + size_t slot = g_z_order[i]; + if (!window_slot_valid(slot) || static_cast(slot) == g_active_slot) { continue; } - render_window(terminal, g_windows[i], false); + render_window(terminal, g_windows[slot], false); } - if (g_active_index >= 0 && static_cast(g_active_index) < g_window_count) + if (g_active_slot >= 0 && window_slot_valid(static_cast(g_active_slot))) { - render_window(terminal, g_windows[g_active_index], true); + render_window(terminal, g_windows[g_active_slot], true); } + + gui::end_window_redraw(); } void request_new_window(Terminal &terminal, Process *proc) @@ -698,25 +1051,27 @@ void request_new_window(Terminal &terminal, Process *proc) return; } - Window &window = g_windows[g_window_count]; - window.in_use = true; + size_t slot = allocate_window_slot(); + if (slot >= MAX_WINDOWS) + { + return; + } + + Window &window = g_windows[slot]; window.owner = proc; window.snapshot = g_blank_snapshot; + reset_dirty(window.dirty); - const size_t cascade_index = g_window_count; + const size_t cascade_index = g_z_count; uint32_t frame_x = INITIAL_FRAME_X + static_cast(cascade_index) * CASCADE_STEP_X; uint32_t frame_y = INITIAL_FRAME_Y + static_cast(cascade_index) * CASCADE_STEP_Y; clamp_frame(frame_x, frame_y); window.frame_x = frame_x; window.frame_y = frame_y; - debug("[GUI] window slot=%u frame=(%u,%u)", static_cast(g_window_count), frame_x, frame_y); - - ++g_window_count; + debug("[GUI] window slot=%u frame=(%u,%u)", static_cast(slot), frame_x, frame_y); - debug("[GUI] focus window index=%u", static_cast(g_window_count - 1)); - focus_window(g_window_count - 1, terminal); - draw_windows(terminal); + focus_window(slot, terminal); } void activate_process(Process *proc, Terminal &terminal) @@ -736,7 +1091,6 @@ void activate_process(Process *proc, Terminal &terminal) } focus_window(static_cast(index), terminal); - draw_windows(terminal); } void set_active_window_origin(Terminal &terminal, Process *proc, int32_t x, int32_t y) @@ -748,13 +1102,13 @@ void set_active_window_origin(Terminal &terminal, Process *proc, int32_t x, int3 ensure_geometry(terminal); - int index = find_window_index(proc); - if (index < 0) + int slot = find_window_index(proc); + if (slot < 0) { return; } - Window &window = g_windows[index]; + Window &window = g_windows[static_cast(slot)]; uint32_t desired_x = x < 0 ? 0U : static_cast(x); uint32_t desired_y = y < 0 ? 0U : static_cast(y); @@ -777,34 +1131,13 @@ void on_process_exit(Process *proc, Terminal &terminal) return; } - int index = find_window_index(proc); - if (index < 0) + int slot = find_window_index(proc); + if (slot < 0) { return; } - for (size_t i = static_cast(index); i + 1 < g_window_count; ++i) - { - g_windows[i] = g_windows[i + 1]; - } - - if (g_window_count > 0) - { - --g_window_count; - g_windows[g_window_count] = Window{}; - } - - if (g_window_count == 0) - { - g_active_index = -1; - } - else - { - // The new top-most window becomes active. - g_active_index = g_window_count - 1; - } - - gui::draw_workspace(terminal); + close_window_slot(static_cast(slot), terminal); } void write_text(Terminal &terminal, Process *proc, const char *text, size_t length) @@ -826,12 +1159,12 @@ void write_text(Terminal &terminal, Process *proc, const char *text, size_t leng ensure_geometry(terminal); build_blank_snapshot(terminal); - int index = find_window_index(proc); - if (index < 0) + int slot = find_window_index(proc); + if (slot < 0) { request_new_window(terminal, proc); - index = find_window_index(proc); - if (index < 0) + slot = find_window_index(proc); + if (slot < 0) { #ifdef DEBUG serial_printf("[TERMWIN] failed to create/find window for process %s\n", @@ -841,42 +1174,255 @@ void write_text(Terminal &terminal, Process *proc, const char *text, size_t leng } } - Window &window = g_windows[index]; - DirtyRegion dirty = make_empty_dirty_region(); - + Window &window = g_windows[static_cast(slot)]; if (window.snapshot.cursor_active) { - mark_row_dirty(dirty, window.snapshot.cursor_row); + mark_row_dirty(window.dirty, window.snapshot.cursor_row); } - snapshot_write(window.snapshot, text, length, dirty); + snapshot_write(window.snapshot, text, length, window.dirty); - mark_row_dirty(dirty, window.snapshot.cursor_row); + mark_row_dirty(window.dirty, window.snapshot.cursor_row); + if (static_cast(slot) == g_active_slot) + { + window_present(proc); + } +} - const bool is_active_window = (index == g_active_index); - if (!dirty_region_has_updates(dirty)) +void window_present(Process *proc) +{ + if (proc == nullptr) { return; } - if (!is_active_window) + int slot = find_window_index(proc); + if (slot < 0) + { + return; + } + + present_window_slot(static_cast(slot)); +} + +void window_put_char(Process *proc, size_t x, size_t y, char ch, uint8_t color) +{ + if (proc == nullptr) { - if (dirty.full_refresh) - { - // No immediate redraw for inactive windows; they'll repaint on focus. - } return; } - if (dirty.full_refresh) + int slot = find_window_index(proc); + if (slot < 0) { - draw_snapshot_contents(window, true); + return; } - else + + if (x >= Terminal::VGA_WIDTH || y >= Terminal::VGA_HEIGHT) { - draw_snapshot_contents(window, true, dirty.min_row, dirty.max_row); + return; + } + + Window &window = g_windows[static_cast(slot)]; + if (!window.in_use) + { + return; + } + + if (window.snapshot.characters[y][x] == ch && window.snapshot.colors[y][x] == color) + { + return; + } + + window.snapshot.characters[y][x] = ch; + window.snapshot.colors[y][x] = color; + mark_row_dirty(window.dirty, y); +} + +void window_set_cursor(Process *proc, size_t row, size_t column, bool active) +{ + if (proc == nullptr) + { + return; + } + + int slot = find_window_index(proc); + if (slot < 0) + { + return; } + + Window &window = g_windows[static_cast(slot)]; + size_t prev_row = window.snapshot.cursor_row; + bool prev_active = window.snapshot.cursor_active; + if (prev_active && prev_row < Terminal::VGA_HEIGHT) + { + mark_row_dirty(window.dirty, prev_row); + } + + if (row >= Terminal::VGA_HEIGHT) + { + row = Terminal::VGA_HEIGHT - 1; + } + if (column >= Terminal::VGA_WIDTH) + { + column = Terminal::VGA_WIDTH - 1; + } + + window.snapshot.cursor_row = row; + window.snapshot.cursor_column = column; + window.snapshot.cursor_active = active; + + if (active) + { + mark_row_dirty(window.dirty, row); + } +} + +bool window_get_cursor(Process *proc, size_t &row, size_t &column) +{ + row = 0; + column = 0; + if (proc == nullptr) + { + return false; + } + + int slot = find_window_index(proc); + if (slot < 0) + { + return false; + } + + const Window &window = g_windows[static_cast(slot)]; + row = window.snapshot.cursor_row; + column = window.snapshot.cursor_column; + return true; +} + +bool handle_mouse_event(Terminal &terminal, const MouseEvent &event) +{ + if (!framebuffer::is_available()) + { + return false; + } + + ensure_geometry(terminal); + + const uint32_t cursor_x = event.x < 0 ? 0U : static_cast(event.x); + const uint32_t cursor_y = event.y < 0 ? 0U : static_cast(event.y); + + const bool left_changed = (event.changed & MOUSE_BUTTON_LEFT) != 0; + const bool left_pressed = left_changed && (event.buttons & MOUSE_BUTTON_LEFT); + const bool left_released = left_changed && (event.buttons & MOUSE_BUTTON_LEFT) == 0; + + bool consumed = false; + + if (left_pressed) + { + WindowHitResult hit = hit_test_window(cursor_x, cursor_y); + if (hit.slot >= 0 && window_slot_valid(static_cast(hit.slot))) + { + Window &window = g_windows[static_cast(hit.slot)]; + if (window.owner != nullptr) + { + if (scheduler_get_foreground() != window.owner) + { + scheduler_set_foreground(window.owner); + } + else + { + focus_window(static_cast(hit.slot), terminal); + } + } + else + { + focus_window(static_cast(hit.slot), terminal); + } + + if (hit.on_close_button) + { + stop_dragging(); + consumed = true; + if (window.owner != nullptr) + { + kill_process(window.owner); + } + else + { + close_window_slot(static_cast(hit.slot), terminal); + } + return true; + } + + if (g_active_slot >= 0 && window_slot_valid(static_cast(g_active_slot))) + { + Window &active_window = g_windows[static_cast(g_active_slot)]; + if (hit.on_title_bar) + { + g_dragging_window = true; + g_drag_slot = g_active_slot; + g_drag_offset_x = static_cast(cursor_x) - static_cast(active_window.frame_x); + g_drag_offset_y = static_cast(cursor_y) - static_cast(active_window.frame_y); + } + else + { + stop_dragging(); + } + } + + consumed = true; + } + else + { + stop_dragging(); + } + } + else if (left_released) + { + if (g_dragging_window) + { + consumed = true; + } + stop_dragging(); + } + + if (g_dragging_window && g_drag_slot >= 0 && window_slot_valid(static_cast(g_drag_slot))) + { + Window &window = g_windows[static_cast(g_drag_slot)]; + const uint32_t old_frame_x = window.frame_x; + const uint32_t old_frame_y = window.frame_y; + int32_t desired_x = static_cast(cursor_x) - g_drag_offset_x; + int32_t desired_y = static_cast(cursor_y) - g_drag_offset_y; + + if (desired_x < 0) + { + desired_x = 0; + } + if (desired_y < 0) + { + desired_y = 0; + } + + uint32_t frame_x = static_cast(desired_x); + uint32_t frame_y = static_cast(desired_y); + clamp_frame(frame_x, frame_y); + + if (frame_x != old_frame_x || frame_y != old_frame_y) + { + if (g_frame_width > 0 && g_frame_height > 0) + { + gui::fill_background_rect(old_frame_x, old_frame_y, g_frame_width, g_frame_height); + } + window.frame_x = frame_x; + window.frame_y = frame_y; + draw_windows(terminal); + } + return true; + } + + return consumed; } } // namespace terminal_windows diff --git a/src/user/editor.cpp b/src/user/editor.cpp index c6aa9e3..f406a84 100644 --- a/src/user/editor.cpp +++ b/src/user/editor.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -44,10 +46,36 @@ static inline void copy_line(char* dst, const char* src) { dst[EDITOR_LINE_LENGTH - 1] = '\0'; } +void Editor::put_cell(size_t x, size_t y, char ch, uint8_t color) { + if (owner_proc && framebuffer::is_available()) { + graphics::put_char(x, y, ch, color); + } else { + terminal.put_at(ch, color, x, y); + } +} + +void Editor::present_window() { + if (owner_proc && framebuffer::is_available()) { + graphics::present(); + } +} + +void Editor::update_cursor_visual(size_t row, size_t column, bool active) { + if (owner_proc && framebuffer::is_available()) { + graphics::set_cursor(row, column, active); + } else if (active) { + terminal.set_cursor(row, column); + } +} + // Start editing a file void Editor::start(const char* path) { active = true; status_message[0] = '\0'; + owner_proc = scheduler_current_process(); + if (owner_proc && framebuffer::is_available()) { + graphics::ensure_window(); + } if (path) { strncpy(this->path, path, sizeof(this->path) - 1); @@ -222,54 +250,59 @@ void Editor::exit(bool save) { } active = false; // Clear the status-bar row - int y = terminal.get_vga_height() - 1; - size_t width = terminal.get_vga_width(); + int y = static_cast(Terminal::VGA_HEIGHT) - 1; + const size_t width = Terminal::VGA_WIDTH; + const uint8_t fill_color = terminal.make_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK); + update_cursor_visual(0, 0, false); for (size_t x = 0; x < width; ++x) { - terminal.put_at(' ', - terminal.make_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK), - x, y); + put_cell(x, static_cast(y), ' ', fill_color); } + present_window(); // Do not kill the process or print the shell prompt here; leave that to editor_entry } // Draw a single line in the editor void Editor::draw_line(const char* text, int y, bool is_active_line) { const char* prefix = is_active_line ? PREFIX_ACTIVE : PREFIX_INACTIVE; - int prefix_len = is_active_line ? PREFIX_ACTIVE_LEN : PREFIX_INACTIVE_LEN; + const int prefix_len = is_active_line ? PREFIX_ACTIVE_LEN : PREFIX_INACTIVE_LEN; - size_t width = terminal.get_vga_width(); - int max_content = (int)width - prefix_len; + const size_t width = Terminal::VGA_WIDTH; + int max_content = static_cast(width) - prefix_len; if (max_content < 0) max_content = 0; - for (int i = 0; i < prefix_len && i < (int)width; ++i) { - terminal.put_at(prefix[i], - terminal.make_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK), - i, y); + const uint8_t prefix_color = terminal.make_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK); + for (int i = 0; i < prefix_len && i < static_cast(width); ++i) { + put_cell(static_cast(i), static_cast(y), prefix[i], prefix_color); } int len = strlen(text); - if (len > max_content) len = max_content; + if (len > max_content) { + len = max_content; + } + for (int i = 0; i < len; ++i) { char c = text[i] ? text[i] : ' '; - int x = prefix_len + i; - auto color = (is_active_line && i == cursor_col) - ? terminal.make_color(VGA_COLOR_BLACK, VGA_COLOR_WHITE) - : terminal.make_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK); - if (x < (int)width) - terminal.put_at(c, color, x, y); + size_t x = static_cast(prefix_len + i); + if (x >= width) { + break; + } + const bool cursor_here = is_active_line && i == cursor_col; + const uint8_t color = cursor_here + ? terminal.make_color(VGA_COLOR_BLACK, VGA_COLOR_WHITE) + : terminal.make_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK); + put_cell(x, static_cast(y), c, color); } - for (int x = prefix_len + len; x < (int)width; ++x) { - terminal.put_at(' ', - terminal.make_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK), - x, y); + const uint8_t fill_color = terminal.make_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK); + for (size_t x = static_cast(prefix_len + len); x < width; ++x) { + put_cell(x, static_cast(y), ' ', fill_color); } } // Draw the status bar at the bottom void Editor::draw_status_bar() { - int y = terminal.get_vga_height() - 1; - size_t width = terminal.get_vga_width(); + int y = static_cast(Terminal::VGA_HEIGHT) - 1; + size_t width = Terminal::VGA_WIDTH; char line[EDITOR_LINE_LENGTH]; int pos = 0; @@ -307,11 +340,10 @@ void Editor::draw_status_bar() { int len = strlen(line); if (len > (int)width) len = (int)width; - for (int x = 0; x < (int)width; ++x) { + const uint8_t bar_color = terminal.make_color(VGA_COLOR_BLACK, VGA_COLOR_WHITE); + for (int x = 0; x < static_cast(width); ++x) { char c = (x < len) ? line[x] : ' '; - terminal.put_at(c, - terminal.make_color(VGA_COLOR_BLACK, VGA_COLOR_WHITE), - x, y); + put_cell(static_cast(x), static_cast(y), c, bar_color); } } @@ -323,14 +355,15 @@ void Editor::set_status_message(const char* msg) { // Render the editor view void Editor::render() { - int rows = terminal.get_vga_height() - 1; + const int total_rows = static_cast(Terminal::VGA_HEIGHT); + int rows = total_rows - 1; if (cursor_line < viewport_offset) { viewport_offset = cursor_line; } else if (cursor_line >= viewport_offset + rows) { viewport_offset = cursor_line - rows + 1; } - size_t width = terminal.get_vga_width(); + const size_t width = Terminal::VGA_WIDTH; int visible_rows = rows; for (int y = 0; y < visible_rows; ++y) { @@ -338,10 +371,9 @@ void Editor::render() { if (idx < line_count) { draw_line(buffer[idx], y, idx == cursor_line); } else { + const uint8_t fill_color = terminal.make_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK); for (size_t x = 0; x < width; ++x) { - terminal.put_at(' ', - terminal.make_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK), - x, y); + put_cell(x, static_cast(y), ' ', fill_color); } } } @@ -351,7 +383,8 @@ void Editor::render() { int cy = cursor_line - viewport_offset; size_t cursor_x = (size_t)(cursor_col + PREFIX_ACTIVE_LEN); if (cursor_x >= width) cursor_x = width ? (width - 1) : 0; - terminal.set_cursor(cy, cursor_x); + update_cursor_visual(static_cast(cy), cursor_x, true); + present_window(); } // Insert a character at the cursor position