diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 84d6cd8..a9ad340 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 54 +#define MONTAUK_BUILD_NUMBER 57 diff --git a/programs/src/login/Makefile b/programs/src/login/Makefile index 449d982..3e4c11d 100644 --- a/programs/src/login/Makefile +++ b/programs/src/login/Makefile @@ -68,7 +68,6 @@ LDFLAGS := \ SRCS := \ main.cpp \ - login_shared.cpp \ login_wallpaper.cpp \ login_render.cpp \ login_input.cpp \ diff --git a/programs/src/login/login.hpp b/programs/src/login/login.hpp index d4fb469..0534346 100644 --- a/programs/src/login/login.hpp +++ b/programs/src/login/login.hpp @@ -16,6 +16,9 @@ #include #include #include +#include +#include +#include extern "C" { unsigned char* stbi_load_from_memory(const unsigned char* buffer, int len, @@ -29,13 +32,6 @@ enum LoginMode { MODE_LOGIN, }; -struct FieldEditState { - int cursor; - int sel_anchor; - int sel_end; - bool has_selection; -}; - struct LoginState { gui::Framebuffer fb; int screen_w, screen_h; @@ -57,14 +53,20 @@ struct LoginState { Montauk::MouseState mouse; uint8_t prev_buttons; - FieldEditState username_edit; - FieldEditState display_name_edit; - FieldEditState password_edit; - FieldEditState confirm_edit; + gui::mtk::TextInputState username_input; + gui::mtk::TextInputState display_name_input; + gui::mtk::TextInputState password_input; + gui::mtk::TextInputState confirm_input; uint32_t* bg_wallpaper; int bg_wallpaper_w, bg_wallpaper_h; bool has_wallpaper; + + gui::mtk::Theme theme; + // Packed scratch buffer used only when the framebuffer pitch differs from + // width*4, since Canvas assumes a tightly packed buffer. When pitch is + // already packed we draw straight into the framebuffer back buffer. + uint32_t* compose; }; inline constexpr gui::Color BG_COLOR = gui::Color::from_rgb(0x00, 0x00, 0x00); @@ -127,34 +129,14 @@ struct LoginLayout { struct LoginField { char* text; int* len; - int max_len; - bool is_password; - FieldEditState* edit; + int cap; // full buffer size (text capacity incl. null) + bool masked; + gui::mtk::TextInputState* state; }; bool load_login_wallpaper(LoginState* ls); LoginField get_field(LoginState* ls, int field); -void reset_field_edit(FieldEditState* edit); -void clear_field_selection(FieldEditState* edit); -void clear_all_field_selections(LoginState* ls); -void clamp_field_edit(FieldEditState* edit, int len); -void field_sel_range(const FieldEditState* edit, int* out_s, int* out_e); -void start_field_selection(FieldEditState* edit); -void update_field_selection(FieldEditState* edit); -int copy_slice(char* out, int cap, const char* text, int start, int end); -int slice_width(const char* text, int start, int end); -int compute_view_start(const char* text, int len, int max_text_w, int cursor); -int compute_view_end(const char* text, int len, int max_text_w, int view_start); -int field_cursor_from_mouse_x(const char* text, int len, int max_text_w, - int anchor_cursor, int rel_x); -void delete_field_range(char* buf, int* len, int start, int count); -void delete_field_selection(char* buf, int* len, FieldEditState* edit); -int password_mask_advance(); -int password_slice_width(int start, int end); -int compute_password_view_start(int len, int max_text_w, int cursor); -int compute_password_view_end(int len, int max_text_w, int view_start); -int password_cursor_from_mouse_x(int len, int max_text_w, int anchor_cursor, int rel_x); LoginLayout layout_login_screen(LoginState* ls); void draw_login_screen(LoginState* ls); diff --git a/programs/src/login/login_input.cpp b/programs/src/login/login_input.cpp index 8b9cb22..d584212 100644 --- a/programs/src/login/login_input.cpp +++ b/programs/src/login/login_input.cpp @@ -11,27 +11,35 @@ namespace { void transition_to_login_mode(LoginState* ls) { ls->mode = MODE_LOGIN; ls->active_field = 0; - ls->password[0] = '\0'; + // Scrub the full buffers, not just the first byte: plaintext residue must + // not linger in the heap after the credentials are consumed. + montauk::memset(ls->password, 0, sizeof(ls->password)); ls->password_len = 0; - ls->confirm[0] = '\0'; + montauk::memset(ls->confirm, 0, sizeof(ls->confirm)); ls->confirm_len = 0; - reset_field_edit(&ls->password_edit); - reset_field_edit(&ls->confirm_edit); - clear_all_field_selections(ls); + gui::mtk::text_input_reset(ls->password_input); + gui::mtk::text_input_reset(ls->confirm_input); + gui::mtk::text_input_set_cursor(ls->username_input, ls->username_len, + ls->username_len, false); ls->show_error = false; } void finish_desktop_session(LoginState* ls) { montauk::user::clear_session(); - ls->password[0] = '\0'; + montauk::memset(ls->password, 0, sizeof(ls->password)); ls->password_len = 0; - reset_field_edit(&ls->password_edit); - clear_all_field_selections(ls); + gui::mtk::text_input_reset(ls->password_input); ls->active_field = 1; ls->show_error = false; } void launch_desktop_session(LoginState* ls) { + // Authentication already succeeded and the session is set, so the password + // is no longer needed. Wipe it before the desktop runs rather than waiting + // for finish_desktop_session, so it does not sit in the heap for the whole + // session. + montauk::memset(ls->password, 0, sizeof(ls->password)); + ls->password_len = 0; int pid = montauk::spawn("0:/os/desktop.elf", ls->username); if (pid >= 0) { montauk::setuser(pid, ls->username); @@ -40,40 +48,46 @@ void launch_desktop_session(LoginState* ls) { finish_desktop_session(ls); } -void insert_char(char* buf, int* len, int max_len, FieldEditState* edit, char c) { - if (edit->has_selection) delete_field_selection(buf, len, edit); - if (*len >= max_len) return; - for (int i = *len; i > edit->cursor; i--) buf[i] = buf[i - 1]; - buf[edit->cursor] = c; - (*len)++; - buf[*len] = '\0'; - edit->cursor++; - clear_field_selection(edit); -} - -void backspace_char(char* buf, int* len, FieldEditState* edit) { - if (edit->has_selection) { - delete_field_selection(buf, len, edit); - return; +gui::Rect login_field_rect(const LoginLayout& lo, LoginState* ls, int idx) { + if (ls->mode == MODE_FIRST_BOOT) { + switch (idx) { + case 0: return lo.username_field; + case 1: return lo.display_name_field; + case 2: return lo.password_field; + case 3: return lo.confirm_field; + default: break; + } + } else { + switch (idx) { + case 0: return lo.username_field; + case 1: return lo.password_field; + default: break; + } } - if (edit->cursor <= 0) return; - delete_field_range(buf, len, edit->cursor - 1, 1); - edit->cursor--; - clear_field_selection(edit); -} - -void delete_char(char* buf, int* len, FieldEditState* edit) { - if (edit->has_selection) { - delete_field_selection(buf, len, edit); - return; - } - if (edit->cursor >= *len) return; - delete_field_range(buf, len, edit->cursor, 1); - clear_field_selection(edit); + return {0, 0, 0, 0}; } } // namespace +LoginField get_field(LoginState* ls, int field) { + if (ls->mode == MODE_FIRST_BOOT) { + switch (field) { + case 0: return {ls->username, &ls->username_len, (int)sizeof(ls->username), false, &ls->username_input}; + case 1: return {ls->display_name, &ls->display_name_len, (int)sizeof(ls->display_name), false, &ls->display_name_input}; + case 2: return {ls->password, &ls->password_len, (int)sizeof(ls->password), true, &ls->password_input}; + case 3: return {ls->confirm, &ls->confirm_len, (int)sizeof(ls->confirm), true, &ls->confirm_input}; + default: break; + } + } else { + switch (field) { + case 0: return {ls->username, &ls->username_len, (int)sizeof(ls->username), false, &ls->username_input}; + case 1: return {ls->password, &ls->password_len, (int)sizeof(ls->password), true, &ls->password_input}; + default: break; + } + } + return {nullptr, nullptr, 0, false, nullptr}; +} + int max_fields(LoginState* ls) { return ls->mode == MODE_FIRST_BOOT ? 4 : 2; } @@ -131,19 +145,19 @@ bool try_login(LoginState* ls) { void handle_key(LoginState* ls, const Montauk::KeyEvent& key) { if (!key.pressed) return; + // Tab / Shift+Tab cycles fields. if (key.scancode == 0x0F) { int mf = max_fields(ls); - clear_all_field_selections(ls); if (key.shift) ls->active_field = (ls->active_field + mf - 1) % mf; else ls->active_field = (ls->active_field + 1) % mf; + LoginField f = get_field(ls, ls->active_field); + if (f.state && f.len) + gui::mtk::text_input_set_cursor(*f.state, *f.len, *f.len, false); ls->show_error = false; return; } - LoginField field = get_field(ls, ls->active_field); - if (!field.text || !field.len || !field.edit) return; - clamp_field_edit(field.edit, *field.len); - + // Enter submits. if (key.ascii == '\n' || key.ascii == '\r') { if (ls->mode == MODE_FIRST_BOOT) { if (try_first_boot_submit(ls)) transition_to_login_mode(ls); @@ -153,75 +167,14 @@ void handle_key(LoginState* ls, const Montauk::KeyEvent& key) { return; } - if (key.ctrl && (key.ascii == 'a' || key.ascii == 'A')) { - field.edit->cursor = *field.len; - field.edit->sel_anchor = 0; - field.edit->sel_end = *field.len; - field.edit->has_selection = (*field.len > 0); - return; - } - - if (key.scancode == 0x4B) { - if (key.shift) { - start_field_selection(field.edit); - if (field.edit->cursor > 0) field.edit->cursor--; - update_field_selection(field.edit); - } else if (field.edit->has_selection) { - int ss, se; - field_sel_range(field.edit, &ss, &se); - field.edit->cursor = ss; - clear_field_selection(field.edit); - } else if (field.edit->cursor > 0) { - field.edit->cursor--; - } - return; - } - - if (key.scancode == 0x4D) { - if (key.shift) { - start_field_selection(field.edit); - if (field.edit->cursor < *field.len) field.edit->cursor++; - update_field_selection(field.edit); - } else if (field.edit->has_selection) { - int ss, se; - field_sel_range(field.edit, &ss, &se); - field.edit->cursor = se; - clear_field_selection(field.edit); - } else if (field.edit->cursor < *field.len) { - field.edit->cursor++; - } - return; - } - - if (key.scancode == 0x47) { - if (key.shift) start_field_selection(field.edit); - else clear_field_selection(field.edit); - field.edit->cursor = 0; - if (key.shift) update_field_selection(field.edit); - return; - } - - if (key.scancode == 0x4F) { - if (key.shift) start_field_selection(field.edit); - else clear_field_selection(field.edit); - field.edit->cursor = *field.len; - if (key.shift) update_field_selection(field.edit); - return; - } - - if (key.scancode == 0x53) { - delete_char(field.text, field.len, field.edit); - return; - } - - if (key.ascii == '\b' || key.scancode == 0x0E) { - backspace_char(field.text, field.len, field.edit); - return; - } - - if (key.ascii >= 0x20 && key.ascii < 0x7F) { - insert_char(field.text, field.len, field.max_len, field.edit, key.ascii); - } + // Everything else is text editing, handled by the toolkit (cursor moves, + // selection, word-jump, clipboard, backspace/delete, insertion). + LoginField f = get_field(ls, ls->active_field); + if (!f.text || !f.state || !f.len) return; + int result = gui::mtk::text_input_key(*f.state, f.text, f.cap, key, + nullptr, nullptr, f.masked); + if (result & gui::mtk::TEXT_INPUT_CHANGED) + *f.len = gui::mtk::text_input_len(f.text, f.cap); } void handle_mouse(LoginState* ls) { @@ -231,63 +184,46 @@ void handle_mouse(LoginState* ls) { uint8_t prev = ls->prev_buttons; bool left_pressed = (buttons & 0x01) && !(prev & 0x01); - if (!left_pressed) return; LoginLayout lo = layout_login_screen(ls); - auto focus_field = [&](int field_index, const gui::Rect& rect) { - LoginField field = get_field(ls, field_index); - if (!field.text || !field.len || !field.edit) return; - clamp_field_edit(field.edit, *field.len); - int anchor_cursor = (field_index == ls->active_field) ? field.edit->cursor : *field.len; - int rel_x = mx - (rect.x + FIELD_PAD); - int max_text_w = rect.w - FIELD_PAD * 2 - 2; - int cursor = field.is_password - ? password_cursor_from_mouse_x(*field.len, max_text_w, anchor_cursor, rel_x) - : field_cursor_from_mouse_x(field.text, *field.len, max_text_w, anchor_cursor, rel_x); - clear_all_field_selections(ls); - ls->active_field = field_index; - field.edit->cursor = cursor; - clear_field_selection(field.edit); - }; + if (left_pressed) { + // Focus a field, or act on a button. + int clicked = -1; + if (lo.username_field.contains(mx, my)) { + clicked = 0; + } else if (ls->mode == MODE_FIRST_BOOT) { + if (lo.display_name_field.contains(mx, my)) clicked = 1; + else if (lo.password_field.contains(mx, my)) clicked = 2; + else if (lo.confirm_field.contains(mx, my)) clicked = 3; + } else { + if (lo.password_field.contains(mx, my)) clicked = 1; + } - if (lo.username_field.contains(mx, my)) { - focus_field(0, lo.username_field); - return; - } - - if (ls->mode == MODE_FIRST_BOOT) { - if (lo.display_name_field.contains(mx, my)) { - focus_field(1, lo.display_name_field); + if (clicked >= 0) { + ls->active_field = clicked; + ls->show_error = false; + } else if (lo.submit_button.contains(mx, my)) { + if (ls->mode == MODE_FIRST_BOOT) { + if (try_first_boot_submit(ls)) transition_to_login_mode(ls); + } else if (try_login(ls)) { + launch_desktop_session(ls); + } return; - } - if (lo.password_field.contains(mx, my)) { - focus_field(2, lo.password_field); + } else if (lo.shutdown_button.contains(mx, my)) { + montauk::shutdown(); return; - } - if (lo.confirm_field.contains(mx, my)) { - focus_field(3, lo.confirm_field); - return; - } - if (lo.submit_button.contains(mx, my)) { - if (try_first_boot_submit(ls)) transition_to_login_mode(ls); - return; - } - } else { - if (lo.password_field.contains(mx, my)) { - focus_field(1, lo.password_field); - return; - } - if (lo.submit_button.contains(mx, my)) { - if (try_login(ls)) launch_desktop_session(ls); + } else if (lo.reboot_button.contains(mx, my)) { + montauk::reset(); return; } } - if (lo.shutdown_button.contains(mx, my)) { - montauk::shutdown(); - return; - } - if (lo.reboot_button.contains(mx, my)) { - montauk::reset(); + // Route the pointer to the active field for cursor placement and + // drag-selection (press, drag, release are all handled by the toolkit). + LoginField f = get_field(ls, ls->active_field); + if (f.text && f.state) { + gui::Rect r = login_field_rect(lo, ls, ls->active_field); + gui::mtk::text_input_mouse(*f.state, r, f.text, mx, my, buttons, prev, + true, f.masked); } } diff --git a/programs/src/login/login_render.cpp b/programs/src/login/login_render.cpp index 7a85397..632edb1 100644 --- a/programs/src/login/login_render.cpp +++ b/programs/src/login/login_render.cpp @@ -8,169 +8,12 @@ using namespace gui; -namespace { - -void draw_login_background(LoginState* ls) { - Framebuffer& fb = ls->fb; - if (!ls->has_wallpaper) { - fb.clear(BG_COLOR); - return; - } - - uint32_t* dst = fb.buffer(); - int pitch = fb.pitch(); - int row_bytes = ls->screen_w * (int)sizeof(uint32_t); - for (int y = 0; y < ls->screen_h; y++) { - uint32_t* dst_row = (uint32_t*)((uint8_t*)dst + y * pitch); - const uint32_t* src_row = ls->bg_wallpaper + y * ls->bg_wallpaper_w; - montauk::memcpy(dst_row, src_row, row_bytes); - } -} - -Rect offset_rect(Rect rect, int dx, int dy) { +static Rect offset_rect(Rect rect, int dx, int dy) { rect.x += dx; rect.y += dy; return rect; } -void build_display_text(char* out, int cap, const char* text, int len) { - if (cap <= 0) return; - if (len < 0) len = 0; - int pos = 0; - while (pos < len && pos < cap - 1) { - out[pos] = text[pos]; - pos++; - } - out[pos] = '\0'; -} - -void draw_text_slice(Framebuffer& fb, int x, int y, const char* text, - int start, int end, Color fg, - bool highlight = false, Color bg = colors::TRANSPARENT) { - char tmp[65]; - if (copy_slice(tmp, sizeof(tmp), text, start, end) <= 0) return; - if (highlight) draw_text_bg(fb, x, y, tmp, fg, bg); - else draw_text(fb, x, y, tmp, fg); -} - -void draw_password_mask_char(Framebuffer& fb, int x, int y, Color fg, - bool highlight = false, Color bg = colors::TRANSPARENT) { - if (highlight) draw_text_bg(fb, x, y, "*", fg, bg); - else draw_text(fb, x, y, "*", fg); -} - -void draw_rounded_frame(Framebuffer& fb, const Rect& rect, int radius, - Color border, Color fill) { - fill_rounded_rect(fb, rect.x, rect.y, rect.w, rect.h, radius, border); - if (rect.w > 2 && rect.h > 2) { - int inner_radius = radius > 0 ? radius - 1 : 0; - fill_rounded_rect(fb, rect.x + 1, rect.y + 1, rect.w - 2, rect.h - 2, - inner_radius, fill); - } -} - -void draw_field(Framebuffer& fb, const Rect& rect, const char* text, int len, - const FieldEditState* edit, bool is_password, - bool active, bool hovered) { - Color border = active ? FIELD_ACTIVE : (hovered ? FIELD_HOVER : FIELD_BORDER); - Color bg = active ? FIELD_BG_ACTIVE : FIELD_BG; - draw_rounded_frame(fb, rect, 6, border, bg); - - int ty = rect.y + (rect.h - system_font_height()) / 2; - int max_text_w = rect.w - FIELD_PAD * 2 - 2; - int cursor = active && edit ? edit->cursor : len; - - if (is_password) { - int view_start = compute_password_view_start(len, max_text_w, cursor); - int view_end = compute_password_view_end(len, max_text_w, view_start); - int advance = password_mask_advance(); - int sel_s = 0; - int sel_e = 0; - bool has_selection = active && edit && edit->has_selection; - if (has_selection) field_sel_range(edit, &sel_s, &sel_e); - - for (int i = view_start; i < view_end; i++) { - int cx = rect.x + FIELD_PAD + (i - view_start) * advance; - bool selected = has_selection && i >= sel_s && i < sel_e; - draw_password_mask_char(fb, cx, ty, TEXT_COLOR, selected, FIELD_SEL_BG); - } - - if (active && edit) { - int cx = rect.x + FIELD_PAD + password_slice_width(view_start, edit->cursor); - fb.fill_rect(cx, ty, 2, system_font_height(), FIELD_ACTIVE); - } - return; - } - - char display[65]; - build_display_text(display, sizeof(display), text, len); - - int view_start = compute_view_start(display, len, max_text_w, cursor); - int view_end = compute_view_end(display, len, max_text_w, view_start); - int text_x = rect.x + FIELD_PAD; - - if (active && edit && edit->has_selection) { - int sel_s, sel_e; - field_sel_range(edit, &sel_s, &sel_e); - if (sel_s < view_end && sel_e > view_start) { - int prefix_end = sel_s < view_start ? view_start : sel_s; - int hi_start = sel_s > view_start ? sel_s : view_start; - int hi_end = sel_e < view_end ? sel_e : view_end; - - draw_text_slice(fb, text_x, ty, display, view_start, prefix_end, TEXT_COLOR); - text_x += slice_width(display, view_start, prefix_end); - draw_text_slice(fb, text_x, ty, display, hi_start, hi_end, TEXT_COLOR, true, FIELD_SEL_BG); - text_x += slice_width(display, hi_start, hi_end); - draw_text_slice(fb, text_x, ty, display, hi_end, view_end, TEXT_COLOR); - } else { - draw_text_slice(fb, text_x, ty, display, view_start, view_end, TEXT_COLOR); - } - } else { - draw_text_slice(fb, text_x, ty, display, view_start, view_end, TEXT_COLOR); - } - - if (active && edit) { - int cx = rect.x + FIELD_PAD + slice_width(display, view_start, edit->cursor); - fb.fill_rect(cx, ty, 2, system_font_height(), FIELD_ACTIVE); - } -} - -void draw_labeled_field(Framebuffer& fb, const Rect& rect, const char* label, - const char* text, int len, const FieldEditState* edit, - bool is_password, bool active, bool hovered) { - draw_text(fb, rect.x, rect.y - LABEL_GAP - system_font_height(), label, LABEL_COLOR); - draw_field(fb, rect, text, len, edit, is_password, active, hovered); -} - -void draw_button(Framebuffer& fb, const Rect& rect, const char* label, - bool hovered, bool primary) { - if (primary) { - fill_rounded_rect(fb, rect.x, rect.y, rect.w, rect.h, 4, - hovered ? BTN_HOVER : BTN_COLOR); - } else { - draw_rounded_frame(fb, rect, 4, - hovered ? FIELD_HOVER : SECONDARY_BTN_BORDER, - hovered ? SECONDARY_BTN_HOVER : SECONDARY_BTN_BG); - } - int tw = text_width(label); - int tx = rect.x + (rect.w - tw) / 2; - int ty = rect.y + (rect.h - system_font_height()) / 2; - draw_text(fb, tx, ty, label, primary ? BTN_TEXT : TEXT_COLOR); -} - -void draw_power_button(Framebuffer& fb, const Rect& rect, const char* label, - bool hovered) { - draw_rounded_frame(fb, rect, 4, - hovered ? FIELD_HOVER : SECONDARY_BTN_BORDER, - hovered ? SECONDARY_BTN_HOVER : SECONDARY_BTN_BG); - int tw = text_width(label); - int tx = rect.x + (rect.w - tw) / 2; - int ty = rect.y + (rect.h - system_font_height()) / 2; - draw_text(fb, tx, ty, label, TEXT_COLOR); -} - -} // namespace - LoginLayout layout_login_screen(LoginState* ls) { LoginLayout lo = {}; int sfh = system_font_height(); @@ -249,59 +92,85 @@ LoginLayout layout_login_screen(LoginState* ls) { void draw_login_screen(LoginState* ls) { Framebuffer& fb = ls->fb; LoginLayout lo = layout_login_screen(ls); + const mtk::Theme& th = ls->theme; int sfh = system_font_height(); + int sw = ls->screen_w; + int sh = ls->screen_h; - draw_login_background(ls); + // MTK widgets draw through a Canvas, which assumes a tightly packed pixel + // buffer (pitch == width*4). Draw straight into the framebuffer back buffer + // when its pitch is already packed; otherwise compose into the packed + // scratch buffer and copy across (handling pitch) before flipping. + bool packed = fb.pitch() == sw * (int)sizeof(uint32_t); + uint32_t* target = packed ? fb.buffer() : ls->compose; + if (!target) return; + Canvas c(target, sw, sh); - draw_shadow(fb, lo.card.x, lo.card.y, lo.card.w, lo.card.h, 4, colors::SHADOW); - fb.fill_rect(lo.card.x, lo.card.y, lo.card.w, lo.card.h, CARD_BG); - fb.fill_rect(lo.titlebar.x, lo.titlebar.y, lo.titlebar.w, lo.titlebar.h, TITLEBAR_BG); - fb.fill_rect(lo.footer.x, lo.footer.y, lo.footer.w, lo.footer.h, FOOTER_BG); - draw_rect(fb, lo.card.x, lo.card.y, lo.card.w, lo.card.h, CARD_BORDER); - draw_hline(fb, lo.titlebar.x, lo.titlebar.y + lo.titlebar.h - 1, lo.titlebar.w, CARD_BORDER); - draw_hline(fb, lo.footer.x, lo.footer.y, lo.footer.w, CARD_BORDER); + // ==== Background ==== + if (ls->has_wallpaper && ls->bg_wallpaper) { + montauk::memcpy(target, ls->bg_wallpaper, + (uint64_t)sw * sh * sizeof(uint32_t)); + } else { + c.fill(BG_COLOR); + } + + // ==== Card ==== + int so = 4; // drop-shadow offset + c.fill_rect_alpha(lo.card.x + so, lo.card.y + lo.card.h, lo.card.w, so, colors::SHADOW); + c.fill_rect_alpha(lo.card.x + lo.card.w, lo.card.y + so, so, lo.card.h, colors::SHADOW); + c.fill_rect_alpha(lo.card.x + lo.card.w, lo.card.y + lo.card.h, so, so, colors::SHADOW); + + c.fill_rect(lo.card.x, lo.card.y, lo.card.w, lo.card.h, CARD_BG); + c.fill_rect(lo.titlebar.x, lo.titlebar.y, lo.titlebar.w, lo.titlebar.h, TITLEBAR_BG); + c.fill_rect(lo.footer.x, lo.footer.y, lo.footer.w, lo.footer.h, FOOTER_BG); + c.rect(lo.card.x, lo.card.y, lo.card.w, lo.card.h, CARD_BORDER); + c.hline(lo.titlebar.x, lo.titlebar.y + lo.titlebar.h - 1, lo.titlebar.w, CARD_BORDER); + c.hline(lo.footer.x, lo.footer.y, lo.footer.w, CARD_BORDER); int window_tw = text_width(lo.window_title); - draw_text(fb, lo.titlebar.x + (lo.titlebar.w - window_tw) / 2, - lo.titlebar.y + (TITLEBAR_H - sfh) / 2, lo.window_title, TEXT_COLOR); - draw_text(fb, lo.content_x, lo.heading_y, lo.heading, TEXT_COLOR); - draw_text(fb, lo.content_x, lo.subtitle_y, lo.subtitle, LABEL_COLOR); + c.text(lo.titlebar.x + (lo.titlebar.w - window_tw) / 2, + lo.titlebar.y + (TITLEBAR_H - sfh) / 2, lo.window_title, th.text); + c.text(lo.content_x, lo.heading_y, lo.heading, th.text); + c.text(lo.content_x, lo.subtitle_y, lo.subtitle, th.text_subtle); - draw_labeled_field(fb, lo.username_field, "Username", ls->username, ls->username_len, - &ls->username_edit, false, - ls->active_field == 0, - lo.username_field.contains(ls->mouse.x, ls->mouse.y)); + // ==== Fields ==== + auto field = [&](const Rect& r, const char* label, const char* val, + mtk::TextInputState& st, bool masked, bool active) { + c.text(r.x, r.y - LABEL_GAP - sfh, label, th.text_muted); + mtk::draw_text_field(c, r, val, st.cursor, active, masked, th, + active ? st.selection_anchor : -1); + }; + + field(lo.username_field, "Username", ls->username, ls->username_input, + false, ls->active_field == 0); if (ls->mode == MODE_FIRST_BOOT) { - draw_labeled_field(fb, lo.display_name_field, "Display Name", ls->display_name, - ls->display_name_len, &ls->display_name_edit, false, - ls->active_field == 1, - lo.display_name_field.contains(ls->mouse.x, ls->mouse.y)); - draw_labeled_field(fb, lo.password_field, "Password", ls->password, - ls->password_len, &ls->password_edit, true, - ls->active_field == 2, - lo.password_field.contains(ls->mouse.x, ls->mouse.y)); - draw_labeled_field(fb, lo.confirm_field, "Confirm Password", ls->confirm, - ls->confirm_len, &ls->confirm_edit, true, - ls->active_field == 3, - lo.confirm_field.contains(ls->mouse.x, ls->mouse.y)); + field(lo.display_name_field, "Display Name", ls->display_name, + ls->display_name_input, false, ls->active_field == 1); + field(lo.password_field, "Password", ls->password, + ls->password_input, true, ls->active_field == 2); + field(lo.confirm_field, "Confirm Password", ls->confirm, + ls->confirm_input, true, ls->active_field == 3); } else { - draw_labeled_field(fb, lo.password_field, "Password", ls->password, - ls->password_len, &ls->password_edit, true, - ls->active_field == 1, - lo.password_field.contains(ls->mouse.x, ls->mouse.y)); + field(lo.password_field, "Password", ls->password, + ls->password_input, true, ls->active_field == 1); } - if (ls->show_error && lo.error_y >= 0) { - draw_text(fb, lo.content_x, lo.error_y, ls->error_msg, ERROR_COLOR); - } + if (ls->show_error && lo.error_y >= 0) + c.text(lo.content_x, lo.error_y, ls->error_msg, th.danger); - draw_power_button(fb, lo.shutdown_button, "Shut Down", - lo.shutdown_button.contains(ls->mouse.x, ls->mouse.y)); - draw_power_button(fb, lo.reboot_button, "Restart", - lo.reboot_button.contains(ls->mouse.x, ls->mouse.y)); - draw_button(fb, lo.submit_button, lo.submit_label, - lo.submit_button.contains(ls->mouse.x, ls->mouse.y), true); + // ==== Buttons ==== + auto btn = [&](const Rect& r) { + return mtk::widget_state(false, r.contains(ls->mouse.x, ls->mouse.y), true); + }; + mtk::draw_button(c, lo.shutdown_button, "Shut Down", mtk::BUTTON_SECONDARY, + btn(lo.shutdown_button), th); + mtk::draw_button(c, lo.reboot_button, "Restart", mtk::BUTTON_SECONDARY, + btn(lo.reboot_button), th); + mtk::draw_button(c, lo.submit_button, lo.submit_label, mtk::BUTTON_PRIMARY, + btn(lo.submit_button), th); + // ==== Present ==== + if (!packed) fb.copy_from(ls->compose, sw * (int)sizeof(uint32_t)); draw_cursor(fb, ls->mouse.x, ls->mouse.y); fb.flip(); } diff --git a/programs/src/login/login_shared.cpp b/programs/src/login/login_shared.cpp deleted file mode 100644 index 7e1001f..0000000 --- a/programs/src/login/login_shared.cpp +++ /dev/null @@ -1,183 +0,0 @@ -/* - * login_shared.cpp - * Shared field-edit helpers for the MontaukOS login screen - * Copyright (c) 2026 Daniel Hammer -*/ - -#include "login.hpp" - -using namespace gui; - -LoginField get_field(LoginState* ls, int field) { - if (ls->mode == MODE_FIRST_BOOT) { - switch (field) { - case 0: return {ls->username, &ls->username_len, 31, false, &ls->username_edit}; - case 1: return {ls->display_name, &ls->display_name_len, 63, false, &ls->display_name_edit}; - case 2: return {ls->password, &ls->password_len, 63, true, &ls->password_edit}; - case 3: return {ls->confirm, &ls->confirm_len, 63, true, &ls->confirm_edit}; - default: break; - } - } else { - switch (field) { - case 0: return {ls->username, &ls->username_len, 31, false, &ls->username_edit}; - case 1: return {ls->password, &ls->password_len, 63, true, &ls->password_edit}; - default: break; - } - } - return {nullptr, nullptr, 0, false, nullptr}; -} - -void reset_field_edit(FieldEditState* edit) { - edit->cursor = 0; - edit->sel_anchor = 0; - edit->sel_end = 0; - edit->has_selection = false; -} - -void clear_field_selection(FieldEditState* edit) { - edit->has_selection = false; - edit->sel_anchor = edit->cursor; - edit->sel_end = edit->cursor; -} - -void clear_all_field_selections(LoginState* ls) { - clear_field_selection(&ls->username_edit); - clear_field_selection(&ls->display_name_edit); - clear_field_selection(&ls->password_edit); - clear_field_selection(&ls->confirm_edit); -} - -void clamp_field_edit(FieldEditState* edit, int len) { - if (edit->cursor < 0) edit->cursor = 0; - if (edit->cursor > len) edit->cursor = len; - if (edit->sel_anchor < 0) edit->sel_anchor = 0; - if (edit->sel_anchor > len) edit->sel_anchor = len; - if (edit->sel_end < 0) edit->sel_end = 0; - if (edit->sel_end > len) edit->sel_end = len; - if (!edit->has_selection || edit->sel_anchor == edit->sel_end) - clear_field_selection(edit); -} - -void field_sel_range(const FieldEditState* edit, int* out_s, int* out_e) { - if (edit->sel_anchor < edit->sel_end) { - *out_s = edit->sel_anchor; - *out_e = edit->sel_end; - } else { - *out_s = edit->sel_end; - *out_e = edit->sel_anchor; - } -} - -void start_field_selection(FieldEditState* edit) { - if (!edit->has_selection) { - edit->sel_anchor = edit->cursor; - edit->sel_end = edit->cursor; - edit->has_selection = true; - } -} - -void update_field_selection(FieldEditState* edit) { - edit->sel_end = edit->cursor; - if (edit->sel_anchor == edit->sel_end) - clear_field_selection(edit); -} - -int copy_slice(char* out, int cap, const char* text, int start, int end) { - if (cap <= 0) return 0; - if (start < 0) start = 0; - if (end < start) end = start; - int pos = 0; - for (int i = start; i < end && pos < cap - 1; i++) out[pos++] = text[i]; - out[pos] = '\0'; - return pos; -} - -int slice_width(const char* text, int start, int end) { - char tmp[65]; - copy_slice(tmp, sizeof(tmp), text, start, end); - return text_width(tmp); -} - -int compute_view_start(const char* text, int len, int max_text_w, int cursor) { - if (cursor < 0) cursor = 0; - if (cursor > len) cursor = len; - int start = 0; - while (start < cursor && slice_width(text, start, cursor) > max_text_w) start++; - return start; -} - -int compute_view_end(const char* text, int len, int max_text_w, int view_start) { - int end = view_start; - while (end < len && slice_width(text, view_start, end + 1) <= max_text_w) end++; - return end; -} - -int field_cursor_from_mouse_x(const char* text, int len, int max_text_w, - int anchor_cursor, int rel_x) { - int view_start = compute_view_start(text, len, max_text_w, anchor_cursor); - if (rel_x <= 0) return view_start; - int view_end = compute_view_end(text, len, max_text_w, view_start); - int prev_w = 0; - for (int i = view_start; i < view_end; i++) { - int next_w = slice_width(text, view_start, i + 1); - int mid = prev_w + (next_w - prev_w) / 2; - if (rel_x < mid) return i; - prev_w = next_w; - } - return view_end; -} - -void delete_field_range(char* buf, int* len, int start, int count) { - if (count <= 0 || start < 0 || start >= *len) return; - if (start + count > *len) count = *len - start; - for (int i = start; i + count < *len; i++) buf[i] = buf[i + count]; - *len -= count; - buf[*len] = '\0'; -} - -void delete_field_selection(char* buf, int* len, FieldEditState* edit) { - if (!edit->has_selection) return; - int ss, se; - field_sel_range(edit, &ss, &se); - delete_field_range(buf, len, ss, se - ss); - edit->cursor = ss; - clear_field_selection(edit); -} - -int password_mask_advance() { - return text_width("*"); -} - -int password_slice_width(int start, int end) { - if (end < start) end = start; - return (end - start) * password_mask_advance(); -} - -int compute_password_view_start(int len, int max_text_w, int cursor) { - if (cursor < 0) cursor = 0; - if (cursor > len) cursor = len; - int start = 0; - while (start < cursor && password_slice_width(start, cursor) > max_text_w) start++; - return start; -} - -int compute_password_view_end(int len, int max_text_w, int view_start) { - int end = view_start; - while (end < len && password_slice_width(view_start, end + 1) <= max_text_w) end++; - return end; -} - -int password_cursor_from_mouse_x(int len, int max_text_w, int anchor_cursor, int rel_x) { - int view_start = compute_password_view_start(len, max_text_w, anchor_cursor); - if (rel_x <= 0) return view_start; - int view_end = compute_password_view_end(len, max_text_w, view_start); - int advance = password_mask_advance(); - int prev_w = 0; - for (int i = view_start; i < view_end; i++) { - int next_w = prev_w + advance; - int mid = prev_w + (next_w - prev_w) / 2; - if (rel_x < mid) return i; - prev_w = next_w; - } - return view_end; -} diff --git a/programs/src/login/login_wallpaper.cpp b/programs/src/login/login_wallpaper.cpp index 5d697d5..91fc7ca 100644 --- a/programs/src/login/login_wallpaper.cpp +++ b/programs/src/login/login_wallpaper.cpp @@ -20,7 +20,9 @@ static uint8_t dim_component(uint8_t value) { bool load_login_wallpaper(LoginState* ls) { auto doc = montauk::config::load("desktop"); - const char* wp = doc.get_string("wallpaper.path", ""); + char wp[256]; + montauk::strncpy(wp, doc.get_string("wallpaper.path", ""), sizeof(wp)); + doc.destroy(); if (wp[0] == '\0') return false; int fd = montauk::open(wp); diff --git a/programs/src/login/main.cpp b/programs/src/login/main.cpp index 72d31fd..558147d 100644 --- a/programs/src/login/main.cpp +++ b/programs/src/login/main.cpp @@ -41,8 +41,8 @@ static void initialize_login_mode(LoginState* ls) { ls->active_field = 0; montauk::strcpy(ls->username, "admin"); ls->username_len = 5; - ls->username_edit.cursor = ls->username_len; - clear_field_selection(&ls->username_edit); + gui::mtk::text_input_set_cursor(ls->username_input, ls->username_len, + ls->username_len, false); return; } @@ -63,10 +63,20 @@ extern "C" void _start() { montauk::set_mouse_bounds(ls->screen_w - 1, ls->screen_h - 1); load_login_wallpaper(ls); + // MTK theme (picks up the system accent). The compose buffer is only + // needed when the framebuffer pitch is not tightly packed; otherwise the + // renderer draws straight into the framebuffer back buffer. + ls->theme = gui::mtk::make_theme(); + if (ls->fb.pitch() != ls->screen_w * (int)sizeof(uint32_t)) { + ls->compose = (uint32_t*)montauk::alloc( + (uint64_t)ls->screen_w * ls->screen_h * sizeof(uint32_t)); + } + maybe_run_setup_session(); initialize_login_mode(ls); bool first_frame = true; + uint64_t input_serial = montauk::input_wait(0, 0); for (;;) { bool mouse_changed = false; bool key_changed = false; @@ -95,9 +105,20 @@ extern "C" void _start() { if (first_frame || mouse_changed || key_changed) { draw_login_screen(ls); first_frame = false; - montauk::sleep_ms(fast_mouse_path ? 1 : 4); - } else { - montauk::sleep_ms(16); } + + // Event-driven wait: input_wait returns the instant the mouse or + // keyboard ISR reports activity, so the cursor tracks the device with + // no fixed-timer latency. timeoutMs is only an upper bound. Matches + // the desktop's run loop; a plain sleep_ms() here cannot wake early + // and makes every gesture start ~16 ms late. + // + // The login screen has no periodic UI (no clock, no caret blink, no + // animation), so when idle it never needs to wake without input: pass + // UINT64_MAX to block indefinitely and consume 0% CPU until the next + // mouse/keyboard event, instead of waking ~62x/sec for nothing. + uint64_t wait_ms = fast_mouse_path ? 1 + : ((mouse_changed || key_changed) ? 4 : UINT64_MAX); + input_serial = montauk::input_wait(input_serial, wait_ms); } }