feat: Fix USB mouse support, GUI wkipedia app, icon refresh, file manager improvements, more
This commit is contained in:
@@ -63,7 +63,7 @@ LDFLAGS := \
|
||||
|
||||
# ---- Source files ----
|
||||
|
||||
SRCS := main.cpp font_data.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp
|
||||
SRCS := main.cpp font_data.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
@@ -26,6 +26,7 @@ struct FileManagerState {
|
||||
uint64_t last_click_time;
|
||||
Scrollbar scrollbar;
|
||||
DesktopState* desktop;
|
||||
bool grid_view;
|
||||
};
|
||||
|
||||
static constexpr int FM_TOOLBAR_H = 32;
|
||||
@@ -33,6 +34,10 @@ static constexpr int FM_PATHBAR_H = 24;
|
||||
static constexpr int FM_HEADER_H = 20;
|
||||
static constexpr int FM_ITEM_H = 24;
|
||||
static constexpr int FM_SCROLLBAR_W = 12;
|
||||
static constexpr int FM_GRID_CELL_W = 80;
|
||||
static constexpr int FM_GRID_CELL_H = 80;
|
||||
static constexpr int FM_GRID_ICON = 48;
|
||||
static constexpr int FM_GRID_PAD = 4;
|
||||
|
||||
// ============================================================================
|
||||
// File type detection
|
||||
@@ -287,6 +292,32 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle view button (5th toolbar button)
|
||||
{
|
||||
int bx = 120, by = 4;
|
||||
uint32_t btn_px = Color::from_rgb(0xE8, 0xE8, 0xE8).to_pixel();
|
||||
for (int dy = 0; dy < 24 && by + dy < ch; dy++)
|
||||
for (int dx = 0; dx < 24 && bx + dx < cw; dx++)
|
||||
pixels[(by + dy) * cw + (bx + dx)] = btn_px;
|
||||
uint32_t glyph_px = colors::TEXT_COLOR.to_pixel();
|
||||
if (fm->grid_view) {
|
||||
// Draw 4 small squares to indicate grid mode
|
||||
for (int r = 0; r < 2; r++)
|
||||
for (int c = 0; c < 2; c++)
|
||||
for (int dy = 0; dy < 6; dy++)
|
||||
for (int dx = 0; dx < 6; dx++)
|
||||
if (by + 5 + r * 8 + dy < ch && bx + 5 + c * 8 + dx < cw)
|
||||
pixels[(by + 5 + r * 8 + dy) * cw + (bx + 5 + c * 8 + dx)] = glyph_px;
|
||||
} else {
|
||||
// Draw 3 horizontal lines to indicate list mode
|
||||
for (int r = 0; r < 3; r++)
|
||||
for (int dx = 0; dx < 14; dx++)
|
||||
for (int dy = 0; dy < 2; dy++)
|
||||
if (by + 5 + r * 5 + dy < ch && bx + 5 + dx < cw)
|
||||
pixels[(by + 5 + r * 5 + dy) * cw + (bx + 5 + dx)] = glyph_px;
|
||||
}
|
||||
}
|
||||
|
||||
// Toolbar separator
|
||||
int sep_y = FM_TOOLBAR_H - 1;
|
||||
if (sep_y < ch) {
|
||||
@@ -310,100 +341,171 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
pixels[sep_y * cw + x] = sep_px;
|
||||
}
|
||||
|
||||
// ---- Column headers ----
|
||||
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||
uint32_t header_px = Color::from_rgb(0xF8, 0xF8, 0xF8).to_pixel();
|
||||
for (int y = header_y; y < header_y + FM_HEADER_H && y < ch; y++)
|
||||
for (int x = 0; x < cw; x++)
|
||||
pixels[y * cw + x] = header_px;
|
||||
if (fm->grid_view) {
|
||||
// ---- Grid View ----
|
||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||
int list_h = ch - list_y;
|
||||
int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
if (cols < 1) cols = 1;
|
||||
int rows = (fm->entry_count + cols - 1) / cols;
|
||||
int content_h = rows * FM_GRID_CELL_H;
|
||||
|
||||
uint32_t dim_px = Color::from_rgb(0x88, 0x88, 0x88).to_pixel();
|
||||
int name_col_x = 8;
|
||||
int size_col_x = cw - FM_SCROLLBAR_W - 120;
|
||||
int type_col_x = cw - FM_SCROLLBAR_W - 60;
|
||||
// Update scrollbar
|
||||
fm->scrollbar.bounds = {cw - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h};
|
||||
fm->scrollbar.content_height = content_h;
|
||||
fm->scrollbar.view_height = list_h;
|
||||
|
||||
draw_text_to_pixels(pixels, cw, ch, name_col_x, header_y + 2, "Name", dim_px);
|
||||
if (size_col_x > 100)
|
||||
draw_text_to_pixels(pixels, cw, ch, size_col_x, header_y + 2, "Size", dim_px);
|
||||
if (type_col_x > 160)
|
||||
draw_text_to_pixels(pixels, cw, ch, type_col_x, header_y + 2, "Type", dim_px);
|
||||
for (int i = 0; i < fm->entry_count; i++) {
|
||||
int col = i % cols;
|
||||
int row = i / cols;
|
||||
int cell_x = col * FM_GRID_CELL_W;
|
||||
int cell_y = list_y + row * FM_GRID_CELL_H - fm->scrollbar.scroll_offset;
|
||||
|
||||
// Header separator
|
||||
sep_y = header_y + FM_HEADER_H - 1;
|
||||
if (sep_y < ch) {
|
||||
for (int x = 0; x < cw; x++)
|
||||
pixels[sep_y * cw + x] = sep_px;
|
||||
}
|
||||
// Skip if entirely off-screen
|
||||
if (cell_y + FM_GRID_CELL_H <= list_y || cell_y >= ch) continue;
|
||||
|
||||
// Column separator lines
|
||||
if (size_col_x > 100) {
|
||||
for (int y = header_y; y < ch; y++)
|
||||
if (size_col_x - 4 >= 0 && size_col_x - 4 < cw)
|
||||
pixels[y * cw + (size_col_x - 4)] = sep_px;
|
||||
}
|
||||
// Selection highlight
|
||||
if (i == fm->selected) {
|
||||
uint32_t sel_px = colors::MENU_HOVER.to_pixel();
|
||||
for (int y = gui_max(cell_y, list_y); y < gui_min(cell_y + FM_GRID_CELL_H, ch); y++)
|
||||
for (int x = cell_x; x < gui_min(cell_x + FM_GRID_CELL_W, cw - FM_SCROLLBAR_W); x++)
|
||||
pixels[y * cw + x] = sel_px;
|
||||
}
|
||||
|
||||
// ---- File entries ----
|
||||
int list_y = header_y + FM_HEADER_H;
|
||||
int list_h = ch - list_y;
|
||||
int visible_items = list_h / FM_ITEM_H;
|
||||
int content_h = fm->entry_count * FM_ITEM_H;
|
||||
// Large icon centered horizontally
|
||||
int icon_x = cell_x + (FM_GRID_CELL_W - FM_GRID_ICON) / 2;
|
||||
int icon_y = cell_y + FM_GRID_PAD;
|
||||
if (ds && fm->entry_types[i] == 1 && ds->icon_folder_lg.pixels) {
|
||||
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_folder_lg);
|
||||
} else if (ds && fm->entry_types[i] == 2 && ds->icon_exec_lg.pixels) {
|
||||
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_exec_lg);
|
||||
} else if (ds && ds->icon_file_lg.pixels) {
|
||||
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_file_lg);
|
||||
} else {
|
||||
uint32_t icon_px = fm->is_dir[i]
|
||||
? Color::from_rgb(0xFF, 0xBD, 0x2E).to_pixel()
|
||||
: Color::from_rgb(0x90, 0x90, 0x90).to_pixel();
|
||||
for (int dy = 0; dy < FM_GRID_ICON && icon_y + dy < ch && icon_y + dy >= list_y; dy++)
|
||||
for (int dx = 0; dx < FM_GRID_ICON && icon_x + dx < cw; dx++)
|
||||
pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px;
|
||||
}
|
||||
|
||||
// Update scrollbar
|
||||
fm->scrollbar.bounds = {cw - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h};
|
||||
fm->scrollbar.content_height = content_h;
|
||||
fm->scrollbar.view_height = list_h;
|
||||
// Filename centered below icon, truncated if needed
|
||||
char label[16];
|
||||
int nlen = zenith::slen(fm->entry_names[i]);
|
||||
if (nlen > 9) {
|
||||
for (int k = 0; k < 9; k++) label[k] = fm->entry_names[i][k];
|
||||
label[9] = '.';
|
||||
label[10] = '.';
|
||||
label[11] = '\0';
|
||||
} else {
|
||||
zenith::strncpy(label, fm->entry_names[i], 15);
|
||||
}
|
||||
int tw = text_width(label);
|
||||
int tx = cell_x + (FM_GRID_CELL_W - tw) / 2;
|
||||
if (tx < cell_x) tx = cell_x;
|
||||
int ty = icon_y + FM_GRID_ICON + 2;
|
||||
if (ty >= list_y && ty + FONT_HEIGHT <= ch)
|
||||
draw_text_to_pixels(pixels, cw, ch, tx, ty, label, text_px);
|
||||
}
|
||||
} else {
|
||||
// ---- List View ----
|
||||
|
||||
int scroll_items = fm->scrollbar.scroll_offset / FM_ITEM_H;
|
||||
// ---- Column headers ----
|
||||
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||
uint32_t header_px = Color::from_rgb(0xF8, 0xF8, 0xF8).to_pixel();
|
||||
for (int y = header_y; y < header_y + FM_HEADER_H && y < ch; y++)
|
||||
for (int x = 0; x < cw; x++)
|
||||
pixels[y * cw + x] = header_px;
|
||||
|
||||
for (int i = scroll_items; i < fm->entry_count && (i - scroll_items) < visible_items + 1; i++) {
|
||||
int iy = list_y + (i - scroll_items) * FM_ITEM_H - (fm->scrollbar.scroll_offset % FM_ITEM_H);
|
||||
if (iy + FM_ITEM_H <= list_y || iy >= ch) continue;
|
||||
uint32_t dim_px = Color::from_rgb(0x88, 0x88, 0x88).to_pixel();
|
||||
int name_col_x = 8;
|
||||
int size_col_x = cw - FM_SCROLLBAR_W - 120;
|
||||
int type_col_x = cw - FM_SCROLLBAR_W - 60;
|
||||
|
||||
// Highlight selected
|
||||
if (i == fm->selected) {
|
||||
uint32_t sel_px = colors::MENU_HOVER.to_pixel();
|
||||
for (int y = gui_max(iy, list_y); y < gui_min(iy + FM_ITEM_H, ch); y++)
|
||||
for (int x = 0; x < cw - FM_SCROLLBAR_W; x++)
|
||||
pixels[y * cw + x] = sel_px;
|
||||
draw_text_to_pixels(pixels, cw, ch, name_col_x, header_y + 2, "Name", dim_px);
|
||||
if (size_col_x > 100)
|
||||
draw_text_to_pixels(pixels, cw, ch, size_col_x, header_y + 2, "Size", dim_px);
|
||||
if (type_col_x > 160)
|
||||
draw_text_to_pixels(pixels, cw, ch, type_col_x, header_y + 2, "Type", dim_px);
|
||||
|
||||
// Header separator
|
||||
sep_y = header_y + FM_HEADER_H - 1;
|
||||
if (sep_y < ch) {
|
||||
for (int x = 0; x < cw; x++)
|
||||
pixels[sep_y * cw + x] = sep_px;
|
||||
}
|
||||
|
||||
// Icon
|
||||
int icon_x = 8;
|
||||
int icon_y = iy + (FM_ITEM_H - 16) / 2;
|
||||
if (ds && fm->entry_types[i] == 1 && ds->icon_folder.pixels) {
|
||||
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_folder);
|
||||
} else if (ds && fm->entry_types[i] == 2 && ds->icon_exec.pixels) {
|
||||
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_exec);
|
||||
} else if (ds && ds->icon_file.pixels) {
|
||||
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_file);
|
||||
} else {
|
||||
uint32_t icon_px = fm->is_dir[i]
|
||||
? Color::from_rgb(0xFF, 0xBD, 0x2E).to_pixel()
|
||||
: Color::from_rgb(0x90, 0x90, 0x90).to_pixel();
|
||||
for (int dy = 0; dy < 16 && icon_y + dy < ch && icon_y + dy >= list_y; dy++)
|
||||
for (int dx = 0; dx < 16 && icon_x + dx < cw; dx++)
|
||||
pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px;
|
||||
// Column separator lines
|
||||
if (size_col_x > 100) {
|
||||
for (int y = header_y; y < ch; y++)
|
||||
if (size_col_x - 4 >= 0 && size_col_x - 4 < cw)
|
||||
pixels[y * cw + (size_col_x - 4)] = sep_px;
|
||||
}
|
||||
|
||||
// Name
|
||||
int tx = 30;
|
||||
int ty = iy + (FM_ITEM_H - FONT_HEIGHT) / 2;
|
||||
if (ty >= list_y && ty + FONT_HEIGHT <= ch)
|
||||
draw_text_to_pixels(pixels, cw, ch, tx, ty, fm->entry_names[i], text_px);
|
||||
// ---- File entries ----
|
||||
int list_y = header_y + FM_HEADER_H;
|
||||
int list_h = ch - list_y;
|
||||
int visible_items = list_h / FM_ITEM_H;
|
||||
int content_h = fm->entry_count * FM_ITEM_H;
|
||||
|
||||
// Size
|
||||
if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + FONT_HEIGHT <= ch) {
|
||||
char size_str[16];
|
||||
format_size(size_str, fm->entry_sizes[i]);
|
||||
draw_text_to_pixels(pixels, cw, ch, size_col_x, ty, size_str, dim_px);
|
||||
}
|
||||
// Update scrollbar
|
||||
fm->scrollbar.bounds = {cw - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h};
|
||||
fm->scrollbar.content_height = content_h;
|
||||
fm->scrollbar.view_height = list_h;
|
||||
|
||||
// Type
|
||||
if (type_col_x > 160 && ty >= list_y && ty + FONT_HEIGHT <= ch) {
|
||||
const char* type_str = "File";
|
||||
if (fm->entry_types[i] == 1) type_str = "Dir";
|
||||
else if (fm->entry_types[i] == 2) type_str = "Exec";
|
||||
draw_text_to_pixels(pixels, cw, ch, type_col_x, ty, type_str, dim_px);
|
||||
int scroll_items = fm->scrollbar.scroll_offset / FM_ITEM_H;
|
||||
|
||||
for (int i = scroll_items; i < fm->entry_count && (i - scroll_items) < visible_items + 1; i++) {
|
||||
int iy = list_y + (i - scroll_items) * FM_ITEM_H - (fm->scrollbar.scroll_offset % FM_ITEM_H);
|
||||
if (iy + FM_ITEM_H <= list_y || iy >= ch) continue;
|
||||
|
||||
// Highlight selected
|
||||
if (i == fm->selected) {
|
||||
uint32_t sel_px = colors::MENU_HOVER.to_pixel();
|
||||
for (int y = gui_max(iy, list_y); y < gui_min(iy + FM_ITEM_H, ch); y++)
|
||||
for (int x = 0; x < cw - FM_SCROLLBAR_W; x++)
|
||||
pixels[y * cw + x] = sel_px;
|
||||
}
|
||||
|
||||
// Icon
|
||||
int icon_x = 8;
|
||||
int icon_y = iy + (FM_ITEM_H - 16) / 2;
|
||||
if (ds && fm->entry_types[i] == 1 && ds->icon_folder.pixels) {
|
||||
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_folder);
|
||||
} else if (ds && fm->entry_types[i] == 2 && ds->icon_exec.pixels) {
|
||||
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_exec);
|
||||
} else if (ds && ds->icon_file.pixels) {
|
||||
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_file);
|
||||
} else {
|
||||
uint32_t icon_px = fm->is_dir[i]
|
||||
? Color::from_rgb(0xFF, 0xBD, 0x2E).to_pixel()
|
||||
: Color::from_rgb(0x90, 0x90, 0x90).to_pixel();
|
||||
for (int dy = 0; dy < 16 && icon_y + dy < ch && icon_y + dy >= list_y; dy++)
|
||||
for (int dx = 0; dx < 16 && icon_x + dx < cw; dx++)
|
||||
pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px;
|
||||
}
|
||||
|
||||
// Name
|
||||
int tx = 30;
|
||||
int ty = iy + (FM_ITEM_H - FONT_HEIGHT) / 2;
|
||||
if (ty >= list_y && ty + FONT_HEIGHT <= ch)
|
||||
draw_text_to_pixels(pixels, cw, ch, tx, ty, fm->entry_names[i], text_px);
|
||||
|
||||
// Size
|
||||
if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + FONT_HEIGHT <= ch) {
|
||||
char size_str[16];
|
||||
format_size(size_str, fm->entry_sizes[i]);
|
||||
draw_text_to_pixels(pixels, cw, ch, size_col_x, ty, size_str, dim_px);
|
||||
}
|
||||
|
||||
// Type
|
||||
if (type_col_x > 160 && ty >= list_y && ty + FONT_HEIGHT <= ch) {
|
||||
const char* type_str = "File";
|
||||
if (fm->entry_types[i] == 1) type_str = "Dir";
|
||||
else if (fm->entry_types[i] == 2) type_str = "Exec";
|
||||
draw_text_to_pixels(pixels, cw, ch, type_col_x, ty, type_str, dim_px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,42 +559,86 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
else if (local_x >= 32 && local_x < 56) filemanager_go_forward(fm);
|
||||
else if (local_x >= 60 && local_x < 84) filemanager_go_up(fm);
|
||||
else if (local_x >= 88 && local_x < 112) filemanager_go_home(fm);
|
||||
else if (local_x >= 120 && local_x < 144) {
|
||||
fm->grid_view = !fm->grid_view;
|
||||
fm->scrollbar.scroll_offset = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// File list clicks
|
||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
|
||||
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
|
||||
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
|
||||
int clicked_idx = rel_y / FM_ITEM_H;
|
||||
// File clicks (grid vs list)
|
||||
if (fm->grid_view) {
|
||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
|
||||
int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
if (cols < 1) cols = 1;
|
||||
int col = local_x / FM_GRID_CELL_W;
|
||||
int row = (local_y - list_y + fm->scrollbar.scroll_offset) / FM_GRID_CELL_H;
|
||||
int clicked_idx = row * cols + col;
|
||||
|
||||
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
|
||||
uint64_t now = zenith::get_milliseconds();
|
||||
if (clicked_idx >= 0 && clicked_idx < fm->entry_count && col < cols) {
|
||||
uint64_t now = zenith::get_milliseconds();
|
||||
|
||||
// Double-click detection
|
||||
if (fm->last_click_item == clicked_idx &&
|
||||
(now - fm->last_click_time) < 400) {
|
||||
if (fm->is_dir[clicked_idx]) {
|
||||
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
|
||||
if (fm->last_click_item == clicked_idx &&
|
||||
(now - fm->last_click_time) < 400) {
|
||||
if (fm->is_dir[clicked_idx]) {
|
||||
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
|
||||
} else {
|
||||
char fullpath[512];
|
||||
zenith::strcpy(fullpath, fm->current_path);
|
||||
int plen = zenith::slen(fullpath);
|
||||
if (plen > 0 && fullpath[plen - 1] != '/') {
|
||||
str_append(fullpath, "/", 512);
|
||||
}
|
||||
str_append(fullpath, fm->entry_names[clicked_idx], 512);
|
||||
if (fm->desktop) {
|
||||
open_texteditor_with_file(fm->desktop, fullpath);
|
||||
}
|
||||
}
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
} else {
|
||||
// Open file in text editor
|
||||
char fullpath[512];
|
||||
zenith::strcpy(fullpath, fm->current_path);
|
||||
int plen = zenith::slen(fullpath);
|
||||
if (plen > 0 && fullpath[plen - 1] != '/') {
|
||||
str_append(fullpath, "/", 512);
|
||||
}
|
||||
str_append(fullpath, fm->entry_names[clicked_idx], 512);
|
||||
if (fm->desktop) {
|
||||
open_texteditor_with_file(fm->desktop, fullpath);
|
||||
}
|
||||
fm->selected = clicked_idx;
|
||||
fm->last_click_item = clicked_idx;
|
||||
fm->last_click_time = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// List view clicks
|
||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
|
||||
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
|
||||
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
|
||||
int clicked_idx = rel_y / FM_ITEM_H;
|
||||
|
||||
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
|
||||
uint64_t now = zenith::get_milliseconds();
|
||||
|
||||
// Double-click detection
|
||||
if (fm->last_click_item == clicked_idx &&
|
||||
(now - fm->last_click_time) < 400) {
|
||||
if (fm->is_dir[clicked_idx]) {
|
||||
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
|
||||
} else {
|
||||
// Open file in text editor
|
||||
char fullpath[512];
|
||||
zenith::strcpy(fullpath, fm->current_path);
|
||||
int plen = zenith::slen(fullpath);
|
||||
if (plen > 0 && fullpath[plen - 1] != '/') {
|
||||
str_append(fullpath, "/", 512);
|
||||
}
|
||||
str_append(fullpath, fm->entry_names[clicked_idx], 512);
|
||||
if (fm->desktop) {
|
||||
open_texteditor_with_file(fm->desktop, fullpath);
|
||||
}
|
||||
}
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
} else {
|
||||
fm->selected = clicked_idx;
|
||||
fm->last_click_item = clicked_idx;
|
||||
fm->last_click_time = now;
|
||||
}
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
} else {
|
||||
fm->selected = clicked_idx;
|
||||
fm->last_click_item = clicked_idx;
|
||||
fm->last_click_time = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,9 +646,12 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
|
||||
// Scroll handling
|
||||
if (ev.scroll != 0) {
|
||||
int list_y_start = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
|
||||
int list_y_start = fm->grid_view
|
||||
? FM_TOOLBAR_H + FM_PATHBAR_H
|
||||
: FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
|
||||
int scroll_step = fm->grid_view ? FM_GRID_CELL_H : FM_ITEM_H;
|
||||
if (local_y >= list_y_start) {
|
||||
fm->scrollbar.scroll_offset -= ev.scroll * FM_ITEM_H;
|
||||
fm->scrollbar.scroll_offset -= ev.scroll * scroll_step;
|
||||
int ms = fm->scrollbar.max_scroll();
|
||||
if (fm->scrollbar.scroll_offset < 0) fm->scrollbar.scroll_offset = 0;
|
||||
if (fm->scrollbar.scroll_offset > ms) fm->scrollbar.scroll_offset = ms;
|
||||
@@ -522,9 +671,29 @@ static void filemanager_on_key(Window* win, const Zenith::KeyEvent& key) {
|
||||
filemanager_go_up(fm);
|
||||
} else if (key.scancode == 0x48) {
|
||||
// Up arrow
|
||||
if (fm->selected > 0) fm->selected--;
|
||||
if (fm->grid_view) {
|
||||
Rect cr = win->content_rect();
|
||||
int cols = (cr.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
if (cols < 1) cols = 1;
|
||||
if (fm->selected >= cols) fm->selected -= cols;
|
||||
} else {
|
||||
if (fm->selected > 0) fm->selected--;
|
||||
}
|
||||
} else if (key.scancode == 0x50) {
|
||||
// Down arrow
|
||||
if (fm->grid_view) {
|
||||
Rect cr = win->content_rect();
|
||||
int cols = (cr.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
if (cols < 1) cols = 1;
|
||||
if (fm->selected + cols < fm->entry_count) fm->selected += cols;
|
||||
} else {
|
||||
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
||||
}
|
||||
} else if (key.scancode == 0x4B && !key.alt && fm->grid_view) {
|
||||
// Left arrow (grid view only)
|
||||
if (fm->selected > 0) fm->selected--;
|
||||
} else if (key.scancode == 0x4D && !key.alt && fm->grid_view) {
|
||||
// Right arrow (grid view only)
|
||||
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
||||
} else if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
if (fm->selected >= 0 && fm->selected < fm->entry_count) {
|
||||
@@ -565,6 +734,7 @@ void open_filemanager(DesktopState* ds) {
|
||||
fm->history_pos = -1;
|
||||
fm->history_count = 0;
|
||||
fm->desktop = ds;
|
||||
fm->grid_view = true;
|
||||
|
||||
fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100);
|
||||
|
||||
|
||||
@@ -0,0 +1,625 @@
|
||||
/*
|
||||
* app_wiki.cpp
|
||||
* ZenithOS Desktop - Wikipedia client application
|
||||
* Spawns wiki.elf -d as a child process for non-blocking network I/O
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int WIKI_RESP_MAX = 131072; // 128 KB
|
||||
static constexpr int WIKI_MAX_LINES = 4096;
|
||||
static constexpr int WIKI_TOOLBAR_H = 36;
|
||||
static constexpr int WIKI_SCROLLBAR_W = 12;
|
||||
|
||||
// ============================================================================
|
||||
// Wiki state
|
||||
// ============================================================================
|
||||
|
||||
struct WikiDisplayLine {
|
||||
char text[256];
|
||||
uint32_t color;
|
||||
};
|
||||
|
||||
struct WikiState {
|
||||
DesktopState* ds;
|
||||
|
||||
enum Mode { IDLE, FETCHING, DONE, WIKI_ERROR };
|
||||
Mode mode;
|
||||
char searchQuery[256];
|
||||
|
||||
WikiDisplayLine* lines;
|
||||
int lineCount;
|
||||
int lineCap;
|
||||
int scrollY; // scroll offset in lines
|
||||
|
||||
// Child process for non-blocking fetch
|
||||
int child_pid;
|
||||
char* respBuf; // accumulated child output
|
||||
int respPos; // current position in respBuf
|
||||
|
||||
char statusMsg[128];
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// JSON string extraction (for parsing child output)
|
||||
// ============================================================================
|
||||
|
||||
static int slen(const char* s) { int n = 0; while (s[n]) n++; return n; }
|
||||
|
||||
static bool memeq(const char* a, const char* b, int n) {
|
||||
for (int i = 0; i < n; i++) if (a[i] != b[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int wiki_extract_json_string(const char* buf, int len, const char* key,
|
||||
char* out, int maxOut) {
|
||||
int klen = slen(key);
|
||||
|
||||
for (int i = 0; i < len - klen - 3; i++) {
|
||||
if (buf[i] != '"') continue;
|
||||
if (!memeq(buf + i + 1, key, klen)) continue;
|
||||
if (buf[i + 1 + klen] != '"') continue;
|
||||
if (buf[i + 2 + klen] != ':') continue;
|
||||
|
||||
int p = i + 3 + klen;
|
||||
while (p < len && (buf[p] == ' ' || buf[p] == '\t')) p++;
|
||||
if (p >= len || buf[p] != '"') continue;
|
||||
p++;
|
||||
|
||||
int j = 0;
|
||||
while (p < len && j < maxOut - 4) {
|
||||
if (buf[p] == '"') break;
|
||||
if (buf[p] == '\\' && p + 1 < len) {
|
||||
p++;
|
||||
switch (buf[p]) {
|
||||
case '"': out[j++] = '"'; break;
|
||||
case '\\': out[j++] = '\\'; break;
|
||||
case 'n': out[j++] = '\n'; break;
|
||||
case 'r': break;
|
||||
case 't': out[j++] = '\t'; break;
|
||||
case '/': out[j++] = '/'; break;
|
||||
case 'u': {
|
||||
if (p + 4 < len) {
|
||||
unsigned val = 0;
|
||||
for (int k = 1; k <= 4; k++) {
|
||||
char h = buf[p + k];
|
||||
val <<= 4;
|
||||
if (h >= '0' && h <= '9') val |= h - '0';
|
||||
else if (h >= 'a' && h <= 'f') val |= h - 'a' + 10;
|
||||
else if (h >= 'A' && h <= 'F') val |= h - 'A' + 10;
|
||||
}
|
||||
p += 4;
|
||||
if (val < 128) out[j++] = (char)val;
|
||||
else if (val == 0x2013 || val == 0x2014) out[j++] = '-';
|
||||
else if (val == 0x2018 || val == 0x2019) out[j++] = '\'';
|
||||
else if (val == 0x201C || val == 0x201D) out[j++] = '"';
|
||||
else if (val == 0x2026) { out[j++] = '.'; out[j++] = '.'; out[j++] = '.'; }
|
||||
else out[j++] = '?';
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: out[j++] = buf[p]; break;
|
||||
}
|
||||
} else {
|
||||
out[j++] = buf[p];
|
||||
}
|
||||
p++;
|
||||
}
|
||||
out[j] = '\0';
|
||||
return j;
|
||||
}
|
||||
out[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Display line building (word-wrap adapted for pixel widths)
|
||||
// ============================================================================
|
||||
|
||||
static void wiki_add_line(WikiState* ws, const char* text, int len, uint32_t color) {
|
||||
if (ws->lineCount >= ws->lineCap) return;
|
||||
WikiDisplayLine* dl = &ws->lines[ws->lineCount];
|
||||
int copyLen = len;
|
||||
if (copyLen > 255) copyLen = 255;
|
||||
for (int i = 0; i < copyLen; i++) dl->text[i] = text[i];
|
||||
dl->text[copyLen] = '\0';
|
||||
dl->color = color;
|
||||
ws->lineCount++;
|
||||
}
|
||||
|
||||
static void wiki_wrap_text(WikiState* ws, const char* text, int textLen,
|
||||
int maxChars, uint32_t color) {
|
||||
if (textLen <= 0 || maxChars <= 0) return;
|
||||
const char* p = text;
|
||||
const char* end = text + textLen;
|
||||
|
||||
while (p < end && ws->lineCount < ws->lineCap) {
|
||||
while (p < end && *p == ' ') p++;
|
||||
if (p >= end) break;
|
||||
|
||||
const char* lineStart = p;
|
||||
const char* lastSpace = nullptr;
|
||||
int col = 0;
|
||||
|
||||
while (p < end && col < maxChars) {
|
||||
if (*p == ' ') lastSpace = p;
|
||||
p++;
|
||||
col++;
|
||||
}
|
||||
|
||||
if (p >= end) {
|
||||
wiki_add_line(ws, lineStart, (int)(p - lineStart), color);
|
||||
} else if (lastSpace && lastSpace > lineStart) {
|
||||
wiki_add_line(ws, lineStart, (int)(lastSpace - lineStart), color);
|
||||
p = lastSpace + 1;
|
||||
} else {
|
||||
wiki_add_line(ws, lineStart, (int)(p - lineStart), color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void wiki_build_display(WikiState* ws, const char* title,
|
||||
const char* extract, int extractLen, int contentW) {
|
||||
ws->lineCount = 0;
|
||||
ws->scrollY = 0;
|
||||
|
||||
int maxChars = (contentW - 24 - WIKI_SCROLLBAR_W) / FONT_WIDTH;
|
||||
if (maxChars < 20) maxChars = 20;
|
||||
|
||||
uint32_t accent_px = colors::ACCENT.to_pixel();
|
||||
uint32_t green_px = Color::from_rgb(0x2E, 0x7D, 0x32).to_pixel();
|
||||
uint32_t text_px = colors::TEXT_COLOR.to_pixel();
|
||||
|
||||
// Title
|
||||
if (title && title[0]) {
|
||||
wiki_wrap_text(ws, title, slen(title), maxChars, accent_px);
|
||||
}
|
||||
|
||||
// Blank separator
|
||||
if (ws->lineCount > 0)
|
||||
wiki_add_line(ws, "", 0, text_px);
|
||||
|
||||
// Process extract line by line
|
||||
const char* p = extract;
|
||||
const char* end = extract + extractLen;
|
||||
|
||||
while (p < end && ws->lineCount < ws->lineCap) {
|
||||
const char* lineStart = p;
|
||||
while (p < end && *p != '\n') p++;
|
||||
int lineLen = (int)(p - lineStart);
|
||||
if (p < end) p++;
|
||||
|
||||
if (lineLen == 0) {
|
||||
wiki_add_line(ws, "", 0, text_px);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Section header detection (== Title ==)
|
||||
if (lineLen >= 4 && lineStart[0] == '=' && lineStart[1] == '=') {
|
||||
int si = 0;
|
||||
while (si < lineLen && lineStart[si] == '=') si++;
|
||||
while (si < lineLen && lineStart[si] == ' ') si++;
|
||||
int ei = lineLen;
|
||||
while (ei > si && lineStart[ei - 1] == '=') ei--;
|
||||
while (ei > si && lineStart[ei - 1] == ' ') ei--;
|
||||
|
||||
wiki_add_line(ws, "", 0, text_px);
|
||||
if (ei > si) {
|
||||
char secBuf[256];
|
||||
int secLen = ei - si;
|
||||
if (secLen > 255) secLen = 255;
|
||||
for (int i = 0; i < secLen; i++) secBuf[i] = lineStart[si + i];
|
||||
secBuf[secLen] = '\0';
|
||||
wiki_add_line(ws, secBuf, secLen, green_px);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular text
|
||||
wiki_wrap_text(ws, lineStart, lineLen, maxChars, text_px);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Process completed response from child
|
||||
// ============================================================================
|
||||
|
||||
static void wiki_process_response(WikiState* ws) {
|
||||
const char* body = ws->respBuf;
|
||||
int bodyLen = ws->respPos;
|
||||
|
||||
if (bodyLen <= 0) {
|
||||
snprintf(ws->statusMsg, sizeof(ws->statusMsg), "Error: no response from Wikipedia");
|
||||
ws->mode = WikiState::WIKI_ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for error sentinel from child
|
||||
if (bodyLen >= 1 && body[0] == '\x01') {
|
||||
snprintf(ws->statusMsg, sizeof(ws->statusMsg), "Article not found: %s", ws->searchQuery);
|
||||
ws->mode = WikiState::WIKI_ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
static char title[512], extractBuf[131072];
|
||||
wiki_extract_json_string(body, bodyLen, "title", title, sizeof(title));
|
||||
int extractLen = wiki_extract_json_string(body, bodyLen, "extract",
|
||||
extractBuf, sizeof(extractBuf) - 1);
|
||||
|
||||
if (extractLen == 0) {
|
||||
snprintf(ws->statusMsg, sizeof(ws->statusMsg), "No content found for: %s", ws->searchQuery);
|
||||
ws->mode = WikiState::WIKI_ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find content width from the window
|
||||
int contentW = 580;
|
||||
for (int i = 0; i < ws->ds->window_count; i++) {
|
||||
Window* w = &ws->ds->windows[i];
|
||||
if (w->app_data == ws) {
|
||||
Rect cr = w->content_rect();
|
||||
contentW = cr.w;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
wiki_build_display(ws, title, extractBuf, extractLen, contentW);
|
||||
ws->mode = WikiState::DONE;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Callbacks
|
||||
// ============================================================================
|
||||
|
||||
static void wiki_on_draw(Window* win, Framebuffer& fb) {
|
||||
WikiState* ws = (WikiState*)win->app_data;
|
||||
if (!ws) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int cw = cr.w;
|
||||
int ch = cr.h;
|
||||
uint32_t* pixels = win->content;
|
||||
|
||||
// Fill background
|
||||
uint32_t bg_px = colors::WINDOW_BG.to_pixel();
|
||||
for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px;
|
||||
|
||||
// ---- Toolbar background ----
|
||||
uint32_t toolbar_bg = Color::from_rgb(0xF5, 0xF5, 0xF5).to_pixel();
|
||||
for (int y = 0; y < WIKI_TOOLBAR_H && y < ch; y++)
|
||||
for (int x = 0; x < cw; x++)
|
||||
pixels[y * cw + x] = toolbar_bg;
|
||||
|
||||
// Toolbar separator line
|
||||
uint32_t sep_px = colors::BORDER.to_pixel();
|
||||
if (WIKI_TOOLBAR_H < ch)
|
||||
for (int x = 0; x < cw; x++)
|
||||
pixels[WIKI_TOOLBAR_H * cw + x] = sep_px;
|
||||
|
||||
// ---- Draw search box outline ----
|
||||
int tb_x = 8;
|
||||
int tb_y = 6;
|
||||
int tb_w = cw - 90;
|
||||
int tb_h = 24;
|
||||
if (tb_w < 100) tb_w = 100;
|
||||
|
||||
// TextBox background
|
||||
uint32_t white_px = colors::WHITE.to_pixel();
|
||||
uint32_t border_px = colors::BORDER.to_pixel();
|
||||
for (int y = tb_y; y < tb_y + tb_h && y < ch; y++)
|
||||
for (int x = tb_x; x < tb_x + tb_w && x < cw; x++)
|
||||
pixels[y * cw + x] = white_px;
|
||||
// Border
|
||||
for (int x = tb_x; x < tb_x + tb_w && x < cw; x++) {
|
||||
if (tb_y < ch) pixels[tb_y * cw + x] = border_px;
|
||||
if (tb_y + tb_h - 1 < ch) pixels[(tb_y + tb_h - 1) * cw + x] = border_px;
|
||||
}
|
||||
for (int y = tb_y; y < tb_y + tb_h && y < ch; y++) {
|
||||
if (tb_x < cw) pixels[y * cw + tb_x] = border_px;
|
||||
if (tb_x + tb_w - 1 < cw) pixels[y * cw + (tb_x + tb_w - 1)] = border_px;
|
||||
}
|
||||
|
||||
// Search text
|
||||
uint32_t text_px = colors::TEXT_COLOR.to_pixel();
|
||||
draw_text_to_pixels(pixels, cw, ch, tb_x + 4, tb_y + (tb_h - FONT_HEIGHT) / 2,
|
||||
ws->searchQuery, text_px);
|
||||
|
||||
// ---- Search button ----
|
||||
int btn_x = tb_x + tb_w + 6;
|
||||
int btn_y = tb_y;
|
||||
int btn_w = 66;
|
||||
int btn_h = tb_h;
|
||||
uint32_t accent_px = colors::ACCENT.to_pixel();
|
||||
for (int y = btn_y; y < btn_y + btn_h && y < ch; y++)
|
||||
for (int x = btn_x; x < btn_x + btn_w && x < cw; x++)
|
||||
pixels[y * cw + x] = accent_px;
|
||||
// Button text
|
||||
uint32_t white_text = colors::WHITE.to_pixel();
|
||||
const char* btnLabel = "Search";
|
||||
int labelW = zenith::slen(btnLabel) * FONT_WIDTH;
|
||||
draw_text_to_pixels(pixels, cw, ch,
|
||||
btn_x + (btn_w - labelW) / 2,
|
||||
btn_y + (btn_h - FONT_HEIGHT) / 2,
|
||||
btnLabel, white_text);
|
||||
|
||||
// ---- Content area ----
|
||||
int content_y = WIKI_TOOLBAR_H + 1;
|
||||
int content_h = ch - content_y;
|
||||
int visibleLines = content_h / (FONT_HEIGHT + 4);
|
||||
if (visibleLines < 1) visibleLines = 1;
|
||||
|
||||
if (ws->mode == WikiState::FETCHING) {
|
||||
draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16,
|
||||
"Loading...", Color::from_rgb(0x88, 0x88, 0x88).to_pixel());
|
||||
} else if (ws->mode == WikiState::WIKI_ERROR) {
|
||||
draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16,
|
||||
ws->statusMsg, colors::CLOSE_BTN.to_pixel());
|
||||
} else if (ws->mode == WikiState::IDLE) {
|
||||
draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16,
|
||||
"Type a topic and press Enter or click Search.",
|
||||
Color::from_rgb(0x88, 0x88, 0x88).to_pixel());
|
||||
} else if (ws->mode == WikiState::DONE && ws->lineCount > 0) {
|
||||
int y = content_y + 8;
|
||||
int lineH = FONT_HEIGHT + 4;
|
||||
for (int i = ws->scrollY; i < ws->lineCount && y + FONT_HEIGHT < ch; i++) {
|
||||
WikiDisplayLine* dl = &ws->lines[i];
|
||||
if (dl->text[0] != '\0') {
|
||||
draw_text_to_pixels(pixels, cw, ch, 12, y, dl->text, dl->color);
|
||||
}
|
||||
y += lineH;
|
||||
}
|
||||
|
||||
// ---- Scrollbar ----
|
||||
if (ws->lineCount > visibleLines) {
|
||||
int sb_x = cw - WIKI_SCROLLBAR_W;
|
||||
int sb_y = content_y;
|
||||
int sb_h = content_h;
|
||||
|
||||
// Scrollbar track
|
||||
uint32_t sb_bg = colors::SCROLLBAR_BG.to_pixel();
|
||||
for (int y2 = sb_y; y2 < sb_y + sb_h && y2 < ch; y2++)
|
||||
for (int x = sb_x; x < cw; x++)
|
||||
pixels[y2 * cw + x] = sb_bg;
|
||||
|
||||
// Thumb
|
||||
int maxScroll = ws->lineCount - visibleLines;
|
||||
if (maxScroll < 1) maxScroll = 1;
|
||||
int thumbH = (visibleLines * sb_h) / ws->lineCount;
|
||||
if (thumbH < 20) thumbH = 20;
|
||||
int thumbY = sb_y + (ws->scrollY * (sb_h - thumbH)) / maxScroll;
|
||||
|
||||
uint32_t sb_fg = colors::SCROLLBAR_FG.to_pixel();
|
||||
for (int y2 = thumbY; y2 < thumbY + thumbH && y2 < ch; y2++)
|
||||
for (int x = sb_x + 2; x < cw - 2; x++)
|
||||
pixels[y2 * cw + x] = sb_fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void wiki_trigger_search(WikiState* ws) {
|
||||
if (ws->searchQuery[0] == '\0') return;
|
||||
if (ws->mode == WikiState::FETCHING) return; // already fetching
|
||||
|
||||
ws->lineCount = 0;
|
||||
ws->scrollY = 0;
|
||||
ws->respPos = 0;
|
||||
|
||||
// Build args string: "-d <query>"
|
||||
static char args[512];
|
||||
args[0] = '-'; args[1] = 'd'; args[2] = ' ';
|
||||
int qi = 0;
|
||||
while (ws->searchQuery[qi] && qi < 500) {
|
||||
args[3 + qi] = ws->searchQuery[qi];
|
||||
qi++;
|
||||
}
|
||||
args[3 + qi] = '\0';
|
||||
|
||||
ws->child_pid = zenith::spawn_redir("0:/os/wiki.elf", args);
|
||||
if (ws->child_pid <= 0) {
|
||||
snprintf(ws->statusMsg, sizeof(ws->statusMsg), "Error: could not start wiki process");
|
||||
ws->mode = WikiState::WIKI_ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
ws->mode = WikiState::FETCHING;
|
||||
}
|
||||
|
||||
static void wiki_on_mouse(Window* win, MouseEvent& ev) {
|
||||
WikiState* ws = (WikiState*)win->app_data;
|
||||
if (!ws) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int cw = cr.w;
|
||||
int local_x = ev.x - cr.x;
|
||||
int local_y = ev.y - cr.y;
|
||||
|
||||
// Check search button click
|
||||
int tb_w = cw - 90;
|
||||
if (tb_w < 100) tb_w = 100;
|
||||
int btn_x = 8 + tb_w + 6;
|
||||
int btn_y = 6;
|
||||
int btn_w = 66;
|
||||
int btn_h = 24;
|
||||
|
||||
if (ev.left_pressed()) {
|
||||
if (local_x >= btn_x && local_x < btn_x + btn_w &&
|
||||
local_y >= btn_y && local_y < btn_y + btn_h) {
|
||||
wiki_trigger_search(ws);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll wheel
|
||||
if (ev.scroll != 0 && ws->mode == WikiState::DONE && ws->lineCount > 0) {
|
||||
int ch = cr.h;
|
||||
int content_h = ch - WIKI_TOOLBAR_H - 1;
|
||||
int visibleLines = content_h / (FONT_HEIGHT + 4);
|
||||
int maxScroll = ws->lineCount - visibleLines;
|
||||
if (maxScroll < 0) maxScroll = 0;
|
||||
|
||||
ws->scrollY += ev.scroll * 3;
|
||||
if (ws->scrollY < 0) ws->scrollY = 0;
|
||||
if (ws->scrollY > maxScroll) ws->scrollY = maxScroll;
|
||||
}
|
||||
}
|
||||
|
||||
static void wiki_on_key(Window* win, const Zenith::KeyEvent& key) {
|
||||
WikiState* ws = (WikiState*)win->app_data;
|
||||
if (!ws || !key.pressed) return;
|
||||
|
||||
// Enter key triggers search
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
wiki_trigger_search(ws);
|
||||
return;
|
||||
}
|
||||
|
||||
// Page Up / Page Down / arrows
|
||||
if (ws->mode == WikiState::DONE && ws->lineCount > 0) {
|
||||
Rect cr = {0, 0, 0, 0};
|
||||
for (int i = 0; i < ws->ds->window_count; i++) {
|
||||
Window* w = &ws->ds->windows[i];
|
||||
if (w->app_data == ws) {
|
||||
cr = w->content_rect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
int content_h = cr.h - WIKI_TOOLBAR_H - 1;
|
||||
int visibleLines = content_h / (FONT_HEIGHT + 4);
|
||||
if (visibleLines < 1) visibleLines = 1;
|
||||
int maxScroll = ws->lineCount - visibleLines;
|
||||
if (maxScroll < 0) maxScroll = 0;
|
||||
|
||||
if (key.scancode == 0x49) { // Page Up
|
||||
ws->scrollY -= visibleLines;
|
||||
if (ws->scrollY < 0) ws->scrollY = 0;
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x51) { // Page Down
|
||||
ws->scrollY += visibleLines;
|
||||
if (ws->scrollY > maxScroll) ws->scrollY = maxScroll;
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x48) { // Up arrow
|
||||
if (ws->scrollY > 0) ws->scrollY--;
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x50) { // Down arrow
|
||||
if (ws->scrollY < maxScroll) ws->scrollY++;
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x47) { // Home
|
||||
ws->scrollY = 0;
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x4F) { // End
|
||||
ws->scrollY = maxScroll;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Text input for search box
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
int len = zenith::slen(ws->searchQuery);
|
||||
if (len > 0) ws->searchQuery[len - 1] = '\0';
|
||||
} else if (key.ascii >= 32 && key.ascii < 127) {
|
||||
int len = zenith::slen(ws->searchQuery);
|
||||
if (len < 254) {
|
||||
ws->searchQuery[len] = key.ascii;
|
||||
ws->searchQuery[len + 1] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void wiki_on_poll(Window* win) {
|
||||
WikiState* ws = (WikiState*)win->app_data;
|
||||
if (!ws) return;
|
||||
if (ws->mode != WikiState::FETCHING || ws->child_pid <= 0) return;
|
||||
|
||||
// Non-blocking read from child process
|
||||
char buf[4096];
|
||||
int n = zenith::childio_read(ws->child_pid, buf, sizeof(buf));
|
||||
|
||||
if (n > 0) {
|
||||
// Accumulate data, checking for EOT sentinel
|
||||
for (int i = 0; i < n && ws->respPos < WIKI_RESP_MAX - 1; i++) {
|
||||
if (buf[i] == '\x04') {
|
||||
// EOT: child is done, process the response
|
||||
ws->respBuf[ws->respPos] = '\0';
|
||||
ws->child_pid = -1;
|
||||
wiki_process_response(ws);
|
||||
return;
|
||||
}
|
||||
if (buf[i] == '\x01') {
|
||||
// Error sentinel from child
|
||||
ws->child_pid = -1;
|
||||
snprintf(ws->statusMsg, sizeof(ws->statusMsg),
|
||||
"Article not found: %s", ws->searchQuery);
|
||||
ws->mode = WikiState::WIKI_ERROR;
|
||||
return;
|
||||
}
|
||||
ws->respBuf[ws->respPos++] = buf[i];
|
||||
}
|
||||
} else if (n < 0) {
|
||||
// Child process exited — process whatever we accumulated
|
||||
ws->child_pid = -1;
|
||||
if (ws->respPos > 0) {
|
||||
ws->respBuf[ws->respPos] = '\0';
|
||||
wiki_process_response(ws);
|
||||
} else {
|
||||
snprintf(ws->statusMsg, sizeof(ws->statusMsg),
|
||||
"Error: fetch failed for \"%s\"", ws->searchQuery);
|
||||
ws->mode = WikiState::WIKI_ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void wiki_on_close(Window* win) {
|
||||
WikiState* ws = (WikiState*)win->app_data;
|
||||
if (!ws) return;
|
||||
|
||||
if (ws->lines) zenith::mfree(ws->lines);
|
||||
if (ws->respBuf) zenith::mfree(ws->respBuf);
|
||||
zenith::mfree(ws);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Wikipedia launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_wiki(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "Wikipedia", 100, 80, 600, 480);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
WikiState* ws = (WikiState*)zenith::malloc(sizeof(WikiState));
|
||||
zenith::memset(ws, 0, sizeof(WikiState));
|
||||
ws->ds = ds;
|
||||
ws->mode = WikiState::IDLE;
|
||||
ws->searchQuery[0] = '\0';
|
||||
ws->scrollY = 0;
|
||||
ws->child_pid = -1;
|
||||
|
||||
// Allocate display lines
|
||||
ws->lineCap = WIKI_MAX_LINES;
|
||||
ws->lines = (WikiDisplayLine*)zenith::malloc(ws->lineCap * sizeof(WikiDisplayLine));
|
||||
ws->lineCount = 0;
|
||||
|
||||
// Allocate response buffer
|
||||
ws->respBuf = (char*)zenith::malloc(WIKI_RESP_MAX);
|
||||
ws->respPos = 0;
|
||||
|
||||
ws->statusMsg[0] = '\0';
|
||||
|
||||
win->app_data = ws;
|
||||
win->on_draw = wiki_on_draw;
|
||||
win->on_mouse = wiki_on_mouse;
|
||||
win->on_key = wiki_on_key;
|
||||
win->on_poll = wiki_on_poll;
|
||||
win->on_close = wiki_on_close;
|
||||
}
|
||||
@@ -248,3 +248,4 @@ void open_calculator(DesktopState* ds);
|
||||
void open_texteditor(DesktopState* ds);
|
||||
void open_texteditor_with_file(DesktopState* ds, const char* path);
|
||||
void open_klog(DesktopState* ds);
|
||||
void open_wiki(DesktopState* ds);
|
||||
|
||||
@@ -31,23 +31,29 @@ void gui::desktop_init(DesktopState* ds) {
|
||||
zenith::memset(&ds->mouse, 0, sizeof(Zenith::MouseState));
|
||||
zenith::set_mouse_bounds(ds->screen_w - 1, ds->screen_h - 1);
|
||||
|
||||
// Load SVG icons
|
||||
ds->icon_terminal = svg_load("0:/icons/utilities-terminal-symbolic.svg", 20, 20, colors::ICON_COLOR);
|
||||
ds->icon_filemanager = svg_load("0:/icons/system-file-manager-symbolic.svg", 20, 20, colors::ICON_COLOR);
|
||||
ds->icon_sysinfo = svg_load("0:/icons/preferences-desktop-apps-symbolic.svg", 20, 20, colors::ICON_COLOR);
|
||||
ds->icon_appmenu = svg_load("0:/icons/view-app-grid-symbolic.svg", 20, 20, colors::PANEL_TEXT);
|
||||
ds->icon_folder = svg_load("0:/icons/folder-symbolic.svg", 16, 16, Color::from_rgb(0xFF, 0xBD, 0x2E));
|
||||
ds->icon_file = svg_load("0:/icons/text-x-generic-symbolic.svg", 16, 16, colors::ICON_COLOR);
|
||||
ds->icon_computer = svg_load("0:/icons/computer-symbolic.svg", 20, 20, colors::ICON_COLOR);
|
||||
ds->icon_network = svg_load("0:/icons/network-wired-symbolic.svg", 16, 16, colors::PANEL_TEXT);
|
||||
ds->icon_calculator = svg_load("0:/icons/accessories-calculator-symbolic.svg", 20, 20, colors::ICON_COLOR);
|
||||
ds->icon_texteditor = svg_load("0:/icons/accessories-text-editor-symbolic.svg", 20, 20, colors::ICON_COLOR);
|
||||
ds->icon_go_up = svg_load("0:/icons/go-up-symbolic.svg", 16, 16, colors::ICON_COLOR);
|
||||
ds->icon_go_back = svg_load("0:/icons/go-previous-symbolic.svg", 16, 16, colors::ICON_COLOR);
|
||||
ds->icon_go_forward = svg_load("0:/icons/go-next-symbolic.svg", 16, 16, colors::ICON_COLOR);
|
||||
ds->icon_save = svg_load("0:/icons/document-save-symbolic.svg", 16, 16, colors::ICON_COLOR);
|
||||
ds->icon_home = svg_load("0:/icons/user-home-symbolic.svg", 16, 16, colors::ICON_COLOR);
|
||||
ds->icon_exec = svg_load("0:/icons/application-x-executable-symbolic.svg", 16, 16, Color::from_rgb(0x4E, 0x9A, 0x06));
|
||||
// Load SVG icons — scalable (colorful) for app menu, symbolic for toolbar/panel
|
||||
Color defColor = colors::ICON_COLOR;
|
||||
ds->icon_terminal = svg_load("0:/icons/utilities-terminal.svg", 20, 20, defColor);
|
||||
ds->icon_filemanager = svg_load("0:/icons/system-file-manager.svg", 20, 20, defColor);
|
||||
ds->icon_sysinfo = svg_load("0:/icons/preferences-desktop-apps.svg", 20, 20, defColor);
|
||||
ds->icon_appmenu = svg_load("0:/icons/view-app-grid-symbolic.svg", 20, 20, colors::PANEL_TEXT);
|
||||
ds->icon_folder = svg_load("0:/icons/folder.svg", 16, 16, defColor);
|
||||
ds->icon_file = svg_load("0:/icons/text-x-generic.svg", 16, 16, defColor);
|
||||
ds->icon_computer = svg_load("0:/icons/computer.svg", 20, 20, defColor);
|
||||
ds->icon_network = svg_load("0:/icons/network-wired-symbolic.svg", 16, 16, colors::PANEL_TEXT);
|
||||
ds->icon_calculator = svg_load("0:/icons/accessories-calculator.svg", 20, 20, defColor);
|
||||
ds->icon_texteditor = svg_load("0:/icons/accessories-text-editor.svg", 20, 20, defColor);
|
||||
ds->icon_go_up = svg_load("0:/icons/go-up-symbolic.svg", 16, 16, defColor);
|
||||
ds->icon_go_back = svg_load("0:/icons/go-previous-symbolic.svg", 16, 16, defColor);
|
||||
ds->icon_go_forward = svg_load("0:/icons/go-next-symbolic.svg", 16, 16, defColor);
|
||||
ds->icon_save = svg_load("0:/icons/document-save-symbolic.svg", 16, 16, defColor);
|
||||
ds->icon_home = svg_load("0:/icons/user-home.svg", 16, 16, defColor);
|
||||
ds->icon_exec = svg_load("0:/icons/utilities-terminal.svg", 16, 16, defColor);
|
||||
ds->icon_wikipedia = svg_load("0:/icons/web-browser.svg", 20, 20, defColor);
|
||||
|
||||
ds->icon_folder_lg = svg_load("0:/icons/folder.svg", 48, 48, defColor);
|
||||
ds->icon_file_lg = svg_load("0:/icons/text-x-generic.svg", 48, 48, defColor);
|
||||
ds->icon_exec_lg = svg_load("0:/icons/utilities-terminal.svg", 48, 48, defColor);
|
||||
|
||||
ds->net_popup_open = false;
|
||||
zenith::get_netcfg(&ds->cached_net_cfg);
|
||||
@@ -334,7 +340,7 @@ void gui::desktop_draw_panel(DesktopState* ds) {
|
||||
// App Menu (5 items with separator and rounded corners)
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int MENU_ITEM_COUNT = 6;
|
||||
static constexpr int MENU_ITEM_COUNT = 7;
|
||||
static constexpr int MENU_W = 220;
|
||||
static constexpr int MENU_ITEM_H = 40;
|
||||
|
||||
@@ -364,6 +370,7 @@ static void desktop_draw_app_menu(DesktopState* ds) {
|
||||
{ "Calculator", &ds->icon_calculator },
|
||||
{ "Text Editor", &ds->icon_texteditor },
|
||||
{ "Kernel Log", &ds->icon_terminal },
|
||||
{ "Wikipedia", &ds->icon_wikipedia },
|
||||
};
|
||||
|
||||
int mx = ds->mouse.x;
|
||||
@@ -558,6 +565,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
case 3: open_calculator(ds); break;
|
||||
case 4: open_texteditor(ds); break;
|
||||
case 5: open_klog(ds); break;
|
||||
case 6: open_wiki(ds); break;
|
||||
}
|
||||
ds->app_menu_open = false;
|
||||
}
|
||||
@@ -598,7 +606,6 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
ds->app_menu_open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Window indicator buttons
|
||||
int indicator_x = 40;
|
||||
for (int i = 0; i < ds->window_count; i++) {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user