feat: use MTK toolkit widgets in login.elf, improve login.elf draw performance
This commit is contained in:
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 54
|
#define MONTAUK_BUILD_NUMBER 57
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ LDFLAGS := \
|
|||||||
|
|
||||||
SRCS := \
|
SRCS := \
|
||||||
main.cpp \
|
main.cpp \
|
||||||
login_shared.cpp \
|
|
||||||
login_wallpaper.cpp \
|
login_wallpaper.cpp \
|
||||||
login_render.cpp \
|
login_render.cpp \
|
||||||
login_input.cpp \
|
login_input.cpp \
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
#include <gui/framebuffer.hpp>
|
#include <gui/framebuffer.hpp>
|
||||||
#include <gui/draw.hpp>
|
#include <gui/draw.hpp>
|
||||||
#include <gui/font.hpp>
|
#include <gui/font.hpp>
|
||||||
|
#include <gui/canvas.hpp>
|
||||||
|
#include <gui/mtk/theme.hpp>
|
||||||
|
#include <gui/mtk/widgets.hpp>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
unsigned char* stbi_load_from_memory(const unsigned char* buffer, int len,
|
unsigned char* stbi_load_from_memory(const unsigned char* buffer, int len,
|
||||||
@@ -29,13 +32,6 @@ enum LoginMode {
|
|||||||
MODE_LOGIN,
|
MODE_LOGIN,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FieldEditState {
|
|
||||||
int cursor;
|
|
||||||
int sel_anchor;
|
|
||||||
int sel_end;
|
|
||||||
bool has_selection;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct LoginState {
|
struct LoginState {
|
||||||
gui::Framebuffer fb;
|
gui::Framebuffer fb;
|
||||||
int screen_w, screen_h;
|
int screen_w, screen_h;
|
||||||
@@ -57,14 +53,20 @@ struct LoginState {
|
|||||||
|
|
||||||
Montauk::MouseState mouse;
|
Montauk::MouseState mouse;
|
||||||
uint8_t prev_buttons;
|
uint8_t prev_buttons;
|
||||||
FieldEditState username_edit;
|
gui::mtk::TextInputState username_input;
|
||||||
FieldEditState display_name_edit;
|
gui::mtk::TextInputState display_name_input;
|
||||||
FieldEditState password_edit;
|
gui::mtk::TextInputState password_input;
|
||||||
FieldEditState confirm_edit;
|
gui::mtk::TextInputState confirm_input;
|
||||||
|
|
||||||
uint32_t* bg_wallpaper;
|
uint32_t* bg_wallpaper;
|
||||||
int bg_wallpaper_w, bg_wallpaper_h;
|
int bg_wallpaper_w, bg_wallpaper_h;
|
||||||
bool has_wallpaper;
|
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);
|
inline constexpr gui::Color BG_COLOR = gui::Color::from_rgb(0x00, 0x00, 0x00);
|
||||||
@@ -127,34 +129,14 @@ struct LoginLayout {
|
|||||||
struct LoginField {
|
struct LoginField {
|
||||||
char* text;
|
char* text;
|
||||||
int* len;
|
int* len;
|
||||||
int max_len;
|
int cap; // full buffer size (text capacity incl. null)
|
||||||
bool is_password;
|
bool masked;
|
||||||
FieldEditState* edit;
|
gui::mtk::TextInputState* state;
|
||||||
};
|
};
|
||||||
|
|
||||||
bool load_login_wallpaper(LoginState* ls);
|
bool load_login_wallpaper(LoginState* ls);
|
||||||
|
|
||||||
LoginField get_field(LoginState* ls, int field);
|
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);
|
LoginLayout layout_login_screen(LoginState* ls);
|
||||||
void draw_login_screen(LoginState* ls);
|
void draw_login_screen(LoginState* ls);
|
||||||
|
|||||||
@@ -11,27 +11,35 @@ namespace {
|
|||||||
void transition_to_login_mode(LoginState* ls) {
|
void transition_to_login_mode(LoginState* ls) {
|
||||||
ls->mode = MODE_LOGIN;
|
ls->mode = MODE_LOGIN;
|
||||||
ls->active_field = 0;
|
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->password_len = 0;
|
||||||
ls->confirm[0] = '\0';
|
montauk::memset(ls->confirm, 0, sizeof(ls->confirm));
|
||||||
ls->confirm_len = 0;
|
ls->confirm_len = 0;
|
||||||
reset_field_edit(&ls->password_edit);
|
gui::mtk::text_input_reset(ls->password_input);
|
||||||
reset_field_edit(&ls->confirm_edit);
|
gui::mtk::text_input_reset(ls->confirm_input);
|
||||||
clear_all_field_selections(ls);
|
gui::mtk::text_input_set_cursor(ls->username_input, ls->username_len,
|
||||||
|
ls->username_len, false);
|
||||||
ls->show_error = false;
|
ls->show_error = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void finish_desktop_session(LoginState* ls) {
|
void finish_desktop_session(LoginState* ls) {
|
||||||
montauk::user::clear_session();
|
montauk::user::clear_session();
|
||||||
ls->password[0] = '\0';
|
montauk::memset(ls->password, 0, sizeof(ls->password));
|
||||||
ls->password_len = 0;
|
ls->password_len = 0;
|
||||||
reset_field_edit(&ls->password_edit);
|
gui::mtk::text_input_reset(ls->password_input);
|
||||||
clear_all_field_selections(ls);
|
|
||||||
ls->active_field = 1;
|
ls->active_field = 1;
|
||||||
ls->show_error = false;
|
ls->show_error = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void launch_desktop_session(LoginState* ls) {
|
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);
|
int pid = montauk::spawn("0:/os/desktop.elf", ls->username);
|
||||||
if (pid >= 0) {
|
if (pid >= 0) {
|
||||||
montauk::setuser(pid, ls->username);
|
montauk::setuser(pid, ls->username);
|
||||||
@@ -40,40 +48,46 @@ void launch_desktop_session(LoginState* ls) {
|
|||||||
finish_desktop_session(ls);
|
finish_desktop_session(ls);
|
||||||
}
|
}
|
||||||
|
|
||||||
void insert_char(char* buf, int* len, int max_len, FieldEditState* edit, char c) {
|
gui::Rect login_field_rect(const LoginLayout& lo, LoginState* ls, int idx) {
|
||||||
if (edit->has_selection) delete_field_selection(buf, len, edit);
|
if (ls->mode == MODE_FIRST_BOOT) {
|
||||||
if (*len >= max_len) return;
|
switch (idx) {
|
||||||
for (int i = *len; i > edit->cursor; i--) buf[i] = buf[i - 1];
|
case 0: return lo.username_field;
|
||||||
buf[edit->cursor] = c;
|
case 1: return lo.display_name_field;
|
||||||
(*len)++;
|
case 2: return lo.password_field;
|
||||||
buf[*len] = '\0';
|
case 3: return lo.confirm_field;
|
||||||
edit->cursor++;
|
default: break;
|
||||||
clear_field_selection(edit);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
void backspace_char(char* buf, int* len, FieldEditState* edit) {
|
switch (idx) {
|
||||||
if (edit->has_selection) {
|
case 0: return lo.username_field;
|
||||||
delete_field_selection(buf, len, edit);
|
case 1: return lo.password_field;
|
||||||
return;
|
default: break;
|
||||||
}
|
}
|
||||||
if (edit->cursor <= 0) return;
|
|
||||||
delete_field_range(buf, len, edit->cursor - 1, 1);
|
|
||||||
edit->cursor--;
|
|
||||||
clear_field_selection(edit);
|
|
||||||
}
|
}
|
||||||
|
return {0, 0, 0, 0};
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // 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) {
|
int max_fields(LoginState* ls) {
|
||||||
return ls->mode == MODE_FIRST_BOOT ? 4 : 2;
|
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) {
|
void handle_key(LoginState* ls, const Montauk::KeyEvent& key) {
|
||||||
if (!key.pressed) return;
|
if (!key.pressed) return;
|
||||||
|
|
||||||
|
// Tab / Shift+Tab cycles fields.
|
||||||
if (key.scancode == 0x0F) {
|
if (key.scancode == 0x0F) {
|
||||||
int mf = max_fields(ls);
|
int mf = max_fields(ls);
|
||||||
clear_all_field_selections(ls);
|
|
||||||
if (key.shift) ls->active_field = (ls->active_field + mf - 1) % mf;
|
if (key.shift) ls->active_field = (ls->active_field + mf - 1) % mf;
|
||||||
else ls->active_field = (ls->active_field + 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;
|
ls->show_error = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LoginField field = get_field(ls, ls->active_field);
|
// Enter submits.
|
||||||
if (!field.text || !field.len || !field.edit) return;
|
|
||||||
clamp_field_edit(field.edit, *field.len);
|
|
||||||
|
|
||||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||||
if (ls->mode == MODE_FIRST_BOOT) {
|
if (ls->mode == MODE_FIRST_BOOT) {
|
||||||
if (try_first_boot_submit(ls)) transition_to_login_mode(ls);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.ctrl && (key.ascii == 'a' || key.ascii == 'A')) {
|
// Everything else is text editing, handled by the toolkit (cursor moves,
|
||||||
field.edit->cursor = *field.len;
|
// selection, word-jump, clipboard, backspace/delete, insertion).
|
||||||
field.edit->sel_anchor = 0;
|
LoginField f = get_field(ls, ls->active_field);
|
||||||
field.edit->sel_end = *field.len;
|
if (!f.text || !f.state || !f.len) return;
|
||||||
field.edit->has_selection = (*field.len > 0);
|
int result = gui::mtk::text_input_key(*f.state, f.text, f.cap, key,
|
||||||
return;
|
nullptr, nullptr, f.masked);
|
||||||
}
|
if (result & gui::mtk::TEXT_INPUT_CHANGED)
|
||||||
|
*f.len = gui::mtk::text_input_len(f.text, f.cap);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void handle_mouse(LoginState* ls) {
|
void handle_mouse(LoginState* ls) {
|
||||||
@@ -231,63 +184,46 @@ void handle_mouse(LoginState* ls) {
|
|||||||
uint8_t prev = ls->prev_buttons;
|
uint8_t prev = ls->prev_buttons;
|
||||||
bool left_pressed = (buttons & 0x01) && !(prev & 0x01);
|
bool left_pressed = (buttons & 0x01) && !(prev & 0x01);
|
||||||
|
|
||||||
if (!left_pressed) return;
|
|
||||||
LoginLayout lo = layout_login_screen(ls);
|
LoginLayout lo = layout_login_screen(ls);
|
||||||
|
|
||||||
auto focus_field = [&](int field_index, const gui::Rect& rect) {
|
if (left_pressed) {
|
||||||
LoginField field = get_field(ls, field_index);
|
// Focus a field, or act on a button.
|
||||||
if (!field.text || !field.len || !field.edit) return;
|
int clicked = -1;
|
||||||
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 (lo.username_field.contains(mx, my)) {
|
if (lo.username_field.contains(mx, my)) {
|
||||||
focus_field(0, lo.username_field);
|
clicked = 0;
|
||||||
return;
|
} 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;
|
||||||
if (ls->mode == MODE_FIRST_BOOT) {
|
else if (lo.confirm_field.contains(mx, my)) clicked = 3;
|
||||||
if (lo.display_name_field.contains(mx, my)) {
|
|
||||||
focus_field(1, lo.display_name_field);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (lo.password_field.contains(mx, my)) {
|
|
||||||
focus_field(2, lo.password_field);
|
|
||||||
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 {
|
} else {
|
||||||
if (lo.password_field.contains(mx, my)) {
|
if (lo.password_field.contains(mx, my)) clicked = 1;
|
||||||
focus_field(1, lo.password_field);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (lo.submit_button.contains(mx, my)) {
|
|
||||||
if (try_login(ls)) launch_desktop_session(ls);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lo.shutdown_button.contains(mx, my)) {
|
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;
|
||||||
|
} else if (lo.shutdown_button.contains(mx, my)) {
|
||||||
montauk::shutdown();
|
montauk::shutdown();
|
||||||
return;
|
return;
|
||||||
}
|
} else if (lo.reboot_button.contains(mx, my)) {
|
||||||
if (lo.reboot_button.contains(mx, my)) {
|
|
||||||
montauk::reset();
|
montauk::reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,169 +8,12 @@
|
|||||||
|
|
||||||
using namespace gui;
|
using namespace gui;
|
||||||
|
|
||||||
namespace {
|
static Rect offset_rect(Rect rect, int dx, int dy) {
|
||||||
|
|
||||||
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) {
|
|
||||||
rect.x += dx;
|
rect.x += dx;
|
||||||
rect.y += dy;
|
rect.y += dy;
|
||||||
return rect;
|
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 layout_login_screen(LoginState* ls) {
|
||||||
LoginLayout lo = {};
|
LoginLayout lo = {};
|
||||||
int sfh = system_font_height();
|
int sfh = system_font_height();
|
||||||
@@ -249,59 +92,85 @@ LoginLayout layout_login_screen(LoginState* ls) {
|
|||||||
void draw_login_screen(LoginState* ls) {
|
void draw_login_screen(LoginState* ls) {
|
||||||
Framebuffer& fb = ls->fb;
|
Framebuffer& fb = ls->fb;
|
||||||
LoginLayout lo = layout_login_screen(ls);
|
LoginLayout lo = layout_login_screen(ls);
|
||||||
|
const mtk::Theme& th = ls->theme;
|
||||||
int sfh = system_font_height();
|
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);
|
// ==== Background ====
|
||||||
fb.fill_rect(lo.card.x, lo.card.y, lo.card.w, lo.card.h, CARD_BG);
|
if (ls->has_wallpaper && ls->bg_wallpaper) {
|
||||||
fb.fill_rect(lo.titlebar.x, lo.titlebar.y, lo.titlebar.w, lo.titlebar.h, TITLEBAR_BG);
|
montauk::memcpy(target, ls->bg_wallpaper,
|
||||||
fb.fill_rect(lo.footer.x, lo.footer.y, lo.footer.w, lo.footer.h, FOOTER_BG);
|
(uint64_t)sw * sh * sizeof(uint32_t));
|
||||||
draw_rect(fb, lo.card.x, lo.card.y, lo.card.w, lo.card.h, CARD_BORDER);
|
} else {
|
||||||
draw_hline(fb, lo.titlebar.x, lo.titlebar.y + lo.titlebar.h - 1, lo.titlebar.w, CARD_BORDER);
|
c.fill(BG_COLOR);
|
||||||
draw_hline(fb, lo.footer.x, lo.footer.y, lo.footer.w, CARD_BORDER);
|
}
|
||||||
|
|
||||||
|
// ==== 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);
|
int window_tw = text_width(lo.window_title);
|
||||||
draw_text(fb, lo.titlebar.x + (lo.titlebar.w - window_tw) / 2,
|
c.text(lo.titlebar.x + (lo.titlebar.w - window_tw) / 2,
|
||||||
lo.titlebar.y + (TITLEBAR_H - sfh) / 2, lo.window_title, TEXT_COLOR);
|
lo.titlebar.y + (TITLEBAR_H - sfh) / 2, lo.window_title, th.text);
|
||||||
draw_text(fb, lo.content_x, lo.heading_y, lo.heading, TEXT_COLOR);
|
c.text(lo.content_x, lo.heading_y, lo.heading, th.text);
|
||||||
draw_text(fb, lo.content_x, lo.subtitle_y, lo.subtitle, LABEL_COLOR);
|
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,
|
// ==== Fields ====
|
||||||
&ls->username_edit, false,
|
auto field = [&](const Rect& r, const char* label, const char* val,
|
||||||
ls->active_field == 0,
|
mtk::TextInputState& st, bool masked, bool active) {
|
||||||
lo.username_field.contains(ls->mouse.x, ls->mouse.y));
|
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) {
|
if (ls->mode == MODE_FIRST_BOOT) {
|
||||||
draw_labeled_field(fb, lo.display_name_field, "Display Name", ls->display_name,
|
field(lo.display_name_field, "Display Name", ls->display_name,
|
||||||
ls->display_name_len, &ls->display_name_edit, false,
|
ls->display_name_input, false, ls->active_field == 1);
|
||||||
ls->active_field == 1,
|
field(lo.password_field, "Password", ls->password,
|
||||||
lo.display_name_field.contains(ls->mouse.x, ls->mouse.y));
|
ls->password_input, true, ls->active_field == 2);
|
||||||
draw_labeled_field(fb, lo.password_field, "Password", ls->password,
|
field(lo.confirm_field, "Confirm Password", ls->confirm,
|
||||||
ls->password_len, &ls->password_edit, true,
|
ls->confirm_input, true, ls->active_field == 3);
|
||||||
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));
|
|
||||||
} else {
|
} else {
|
||||||
draw_labeled_field(fb, lo.password_field, "Password", ls->password,
|
field(lo.password_field, "Password", ls->password,
|
||||||
ls->password_len, &ls->password_edit, true,
|
ls->password_input, true, ls->active_field == 1);
|
||||||
ls->active_field == 1,
|
|
||||||
lo.password_field.contains(ls->mouse.x, ls->mouse.y));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ls->show_error && lo.error_y >= 0) {
|
if (ls->show_error && lo.error_y >= 0)
|
||||||
draw_text(fb, lo.content_x, lo.error_y, ls->error_msg, ERROR_COLOR);
|
c.text(lo.content_x, lo.error_y, ls->error_msg, th.danger);
|
||||||
}
|
|
||||||
|
|
||||||
draw_power_button(fb, lo.shutdown_button, "Shut Down",
|
// ==== Buttons ====
|
||||||
lo.shutdown_button.contains(ls->mouse.x, ls->mouse.y));
|
auto btn = [&](const Rect& r) {
|
||||||
draw_power_button(fb, lo.reboot_button, "Restart",
|
return mtk::widget_state(false, r.contains(ls->mouse.x, ls->mouse.y), true);
|
||||||
lo.reboot_button.contains(ls->mouse.x, ls->mouse.y));
|
};
|
||||||
draw_button(fb, lo.submit_button, lo.submit_label,
|
mtk::draw_button(c, lo.shutdown_button, "Shut Down", mtk::BUTTON_SECONDARY,
|
||||||
lo.submit_button.contains(ls->mouse.x, ls->mouse.y), true);
|
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);
|
draw_cursor(fb, ls->mouse.x, ls->mouse.y);
|
||||||
fb.flip();
|
fb.flip();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,9 @@ static uint8_t dim_component(uint8_t value) {
|
|||||||
|
|
||||||
bool load_login_wallpaper(LoginState* ls) {
|
bool load_login_wallpaper(LoginState* ls) {
|
||||||
auto doc = montauk::config::load("desktop");
|
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;
|
if (wp[0] == '\0') return false;
|
||||||
|
|
||||||
int fd = montauk::open(wp);
|
int fd = montauk::open(wp);
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ static void initialize_login_mode(LoginState* ls) {
|
|||||||
ls->active_field = 0;
|
ls->active_field = 0;
|
||||||
montauk::strcpy(ls->username, "admin");
|
montauk::strcpy(ls->username, "admin");
|
||||||
ls->username_len = 5;
|
ls->username_len = 5;
|
||||||
ls->username_edit.cursor = ls->username_len;
|
gui::mtk::text_input_set_cursor(ls->username_input, ls->username_len,
|
||||||
clear_field_selection(&ls->username_edit);
|
ls->username_len, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,10 +63,20 @@ extern "C" void _start() {
|
|||||||
montauk::set_mouse_bounds(ls->screen_w - 1, ls->screen_h - 1);
|
montauk::set_mouse_bounds(ls->screen_w - 1, ls->screen_h - 1);
|
||||||
load_login_wallpaper(ls);
|
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();
|
maybe_run_setup_session();
|
||||||
initialize_login_mode(ls);
|
initialize_login_mode(ls);
|
||||||
|
|
||||||
bool first_frame = true;
|
bool first_frame = true;
|
||||||
|
uint64_t input_serial = montauk::input_wait(0, 0);
|
||||||
for (;;) {
|
for (;;) {
|
||||||
bool mouse_changed = false;
|
bool mouse_changed = false;
|
||||||
bool key_changed = false;
|
bool key_changed = false;
|
||||||
@@ -95,9 +105,20 @@ extern "C" void _start() {
|
|||||||
if (first_frame || mouse_changed || key_changed) {
|
if (first_frame || mouse_changed || key_changed) {
|
||||||
draw_login_screen(ls);
|
draw_login_screen(ls);
|
||||||
first_frame = false;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user