/* * login_render.cpp * Layout and drawing for the MontaukOS login screen * Copyright (c) 2026 Daniel Hammer */ #include "login.hpp" using namespace gui; static Rect offset_rect(Rect rect, int dx, int dy) { rect.x += dx; rect.y += dy; return rect; } LoginLayout layout_login_screen(LoginState* ls) { LoginLayout lo = {}; int sfh = system_font_height(); lo.window_title = ls->mode == MODE_FIRST_BOOT ? "MontaukOS Setup" : "MontaukOS"; lo.heading = ls->mode == MODE_FIRST_BOOT ? "Create Administrator Account" : "Log In"; lo.subtitle = ls->mode == MODE_FIRST_BOOT ? "Create the first administrator account." : "Sign in to continue to the desktop."; lo.submit_label = ls->mode == MODE_FIRST_BOOT ? "Create Account" : "Log In"; lo.content_x = CONTENT_PAD_X; lo.content_w = CARD_W - CONTENT_PAD_X * 2; int y = TITLEBAR_H + CONTENT_TOP_PAD; lo.heading_y = y; y += sfh + 6; lo.subtitle_y = y; y += sfh + 18; lo.username_field = {lo.content_x, y + sfh + LABEL_GAP, lo.content_w, FIELD_H}; y = lo.username_field.y + FIELD_H + FIELD_GAP; if (ls->mode == MODE_FIRST_BOOT) { lo.display_name_field = {lo.content_x, y + sfh + LABEL_GAP, lo.content_w, FIELD_H}; y = lo.display_name_field.y + FIELD_H + FIELD_GAP; lo.password_field = {lo.content_x, y + sfh + LABEL_GAP, lo.content_w, FIELD_H}; y = lo.password_field.y + FIELD_H + FIELD_GAP; lo.confirm_field = {lo.content_x, y + sfh + LABEL_GAP, lo.content_w, FIELD_H}; y = lo.confirm_field.y + FIELD_H + FIELD_GAP; } else { lo.password_field = {lo.content_x, y + sfh + LABEL_GAP, lo.content_w, FIELD_H}; y = lo.password_field.y + FIELD_H + FIELD_GAP; } // Session selector (login mode only): a "Session" label above two radio // options that pick the desktop or the full-screen console. if (ls->mode == MODE_LOGIN) { lo.session_label_y = y; int ry = y + sfh + LABEL_GAP; int radio_h = 22; int half = lo.content_w / 2; lo.desktop_radio = {lo.content_x, ry, half, radio_h}; lo.console_radio = {lo.content_x + half, ry, half, radio_h}; y = ry + radio_h + FIELD_GAP; } else { lo.session_label_y = -1; } if (ls->show_error) { lo.error_y = y; y += sfh + 12; } else { lo.error_y = -1; } int footer_y = y + BODY_BOTTOM_PAD; int card_h = footer_y + FOOTER_H; int card_x = (ls->screen_w - CARD_W) / 2; int card_y = (ls->screen_h - card_h) / 2; lo.card = {card_x, card_y, CARD_W, card_h}; lo.titlebar = {card_x, card_y, CARD_W, TITLEBAR_H}; lo.footer = {card_x, card_y + footer_y, CARD_W, FOOTER_H}; int submit_w = text_width(lo.submit_label) + 32; if (submit_w < 100) submit_w = 100; int footer_btn_y = footer_y + (FOOTER_H - BTN_H) / 2; lo.shutdown_button = {FOOTER_PAD, footer_btn_y, POWER_BTN_W, BTN_H}; lo.reboot_button = {FOOTER_PAD + POWER_BTN_W + POWER_BTN_GAP, footer_btn_y, POWER_BTN_W, BTN_H}; lo.submit_button = {CARD_W - FOOTER_PAD - submit_w, footer_btn_y, submit_w, BTN_H}; lo.username_field = offset_rect(lo.username_field, card_x, card_y); lo.display_name_field = offset_rect(lo.display_name_field, card_x, card_y); lo.password_field = offset_rect(lo.password_field, card_x, card_y); lo.confirm_field = offset_rect(lo.confirm_field, card_x, card_y); lo.shutdown_button = offset_rect(lo.shutdown_button, card_x, card_y); lo.reboot_button = offset_rect(lo.reboot_button, card_x, card_y); lo.submit_button = offset_rect(lo.submit_button, card_x, card_y); lo.desktop_radio = offset_rect(lo.desktop_radio, card_x, card_y); lo.console_radio = offset_rect(lo.console_radio, card_x, card_y); lo.content_x += card_x; lo.heading_y += card_y; lo.subtitle_y += card_y; if (lo.error_y >= 0) lo.error_y += card_y; if (lo.session_label_y >= 0) lo.session_label_y += card_y; return lo; } 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; // 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); // ==== Background ==== if (__atomic_load_n(&ls->has_wallpaper, __ATOMIC_ACQUIRE) && 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); 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); // ==== 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) { 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 { field(lo.password_field, "Password", ls->password, ls->password_input, true, ls->active_field == 1); } // ==== Session selector ==== if (ls->mode == MODE_LOGIN && lo.session_label_y >= 0) { c.text(lo.content_x, lo.session_label_y, "Session", th.text_muted); mtk::draw_radio(c, lo.desktop_radio, "Desktop", ls->session_mode == SESSION_DESKTOP, th); mtk::draw_radio(c, lo.console_radio, "Console", ls->session_mode == SESSION_CONSOLE, th); } if (ls->show_error && lo.error_y >= 0) c.text(lo.content_x, lo.error_y, ls->error_msg, th.danger); // ==== 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(); }