Game UI systems in Rust require different considerations than web development. CSS handles styling and layout automatically, but game engines need explicit rendering, input handling, and cross-platform compatibility. This post covers the UI rendering system built for Mirador and how it implements the pause menu.
The UI system uses a modular, component-based approach. The ButtonManager
coordinates all interactive elements. It separates rendering logic—handled by specialized renderers for text, rectangles, and icons—from input and state management. The system tracks mouse state and position to detect interactions, update visual states efficiently, and trigger actions only when necessary.
pub struct ButtonManager {
pub buttons: HashMap<String, Button>,
pub button_order: Vec<String>,
pub text_renderer: TextRenderer,
pub rectangle_renderer: RectangleRenderer,
pub icon_renderer: IconRenderer,
pub window_size: PhysicalSize<u32>,
pub mouse_position: (f32, f32),
pub mouse_pressed: bool,
pub just_clicked: Option<String>,
pub container_rect: Option<Rectangle>,
pub last_mouse_position: (f32, f32),
pub last_mouse_pressed: bool,
}
The positioning system uses anchor points instead of hardcoded coordinates, simplifying layout calculations. The ButtonPosition
struct stores coordinates, dimensions, and an anchor. Its calculate_actual_position()
method converts anchored coordinates to top-left rendering coordinates, improving maintainability.
#[derive(Debug, Clone)]
pub struct ButtonPosition {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub anchor: ButtonAnchor,
}
pub fn calculate_actual_position(&self) -> (f32, f32) {
let actual_x = match self.anchor {
ButtonAnchor::TopLeft => self.x,
ButtonAnchor::Center => self.x - self.width / 2.0,
};
let actual_y = match self.anchor {
ButtonAnchor::TopLeft => self.y,
ButtonAnchor::Center => self.y - self.height / 2.0,
};
(actual_x, actual_y)
}
The system supports different spacing strategies: Wrap
sizes buttons to their text, Hbar
makes width proportional to container width, and Tall
adjusts height for grid-based layouts. These options enable a single system to handle both compact and structured interfaces.
Buttons are created using a fluent interface that sets their appearance, behavior, and layout. When added to the manager, button dimensions are calculated based on spacing strategy and text size, maintaining visual consistency and hierarchy.
The rectangle renderer uses Signed Distance Functions (SDFs) for resolution-independent corner rounding. The shader calculates distance from each pixel to the rectangle’s edge, considering corner radius. Negative values indicate the pixel is inside the shape, positive values are outside. A smoothstep
blend on this distance creates soft, anti-aliased transitions at edges.
fn sdf_rounded_rect(p: vec2<f32>, size: vec2<f32>, radius: f32) -> f32 {
let half_size = size * 0.5;
let d = abs(p - half_size) - half_size + radius;
return length(max(d, vec2<f32>(0.0))) + min(max(d.x, d.y), 0.0) - radius;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
if (in.corner_radius <= 0.0) {
return in.color;
}
let distance = sdf_rounded_rect(in.uv, in.rect_size, in.corner_radius);
let alpha = 1.0 - smoothstep(-1.0, 1.0, distance);
var output_color = in.color;
output_color.a *= alpha;
return output_color;
}
This method ensures crisp edges at any scale and avoids the pixelation that can occur with texture masks or polygon-based techniques.
Each button tracks a ButtonState
based on interaction. Colors and effects update dynamically to reflect hover or press state. For upgrade menus, subtle scaling adds feedback—buttons grow slightly on hover and shrink when pressed—improving responsiveness without visual clutter.
The pause menu demonstrates how the system components integrate into a cohesive UI. It displays a centered overlay with options to resume, restart, toggle test mode, return to the lobby, or exit. This simple interface showcases the flexibility of the system in a real-world context. Actions are modeled with an enum for type safety:
#[derive(Debug, Clone, PartialEq)]
pub enum PauseMenuAction {
Resume,
Restart,
QuitToMenu,
QuitApp,
ToggleTestMode,
None,
}
Scaling is dynamic. The system uses a reference height of 1080 pixels and clamps the scale factor between 0.7 and 2.0. Button size is proportional to the screen, with bounds to ensure usability across resolutions. This keeps the interface accessible on everything from small laptops to large monitors.
Semantic styles help convey button purpose. Primary actions like "Resume" use neutral styles, while destructive options like "Quit App" use danger colors. This visual language guides users and prevents accidental data loss.
Buttons are vertically stacked with a helper function that centers them and evenly spaces each one. This keeps layout logic clean and easy to update.
let y = |i: usize| start_y + button_height / 2.0 + i as f32 * (button_height + button_spacing);
let resume_button = Button::new("resume", "Resume Game")
.with_style(resume_style)
.with_text_align(TextAlign::Center)
.with_position(
ButtonPosition::new(center_x, y(0), button_width, button_height)
.with_anchor(ButtonAnchor::Center),
);
The ButtonManager
handles all mouse interaction, while the menu logic checks for clicks and dispatches actions, playing audio feedback when triggered. This separation of concerns improves modularity and testability.
There’s also a small debug toggle button in the bottom-left corner, demonstrating how specialized elements can coexist with the main layout.
Rendering is layered: backgrounds first, then icons, then text. This ensures clarity and correct visual hierarchy. Each renderer is optimized for its content type—text, geometry, or icons—enabling better performance than a general-purpose renderer. State change detection ensures button visuals are only updated when input changes, preventing unnecessary rendering. This is especially important for games that require consistent frame rates. DPI scaling ensures UI elements remain sharp and proportionally sized across devices. Combined, these design choices provide a UI system that is modular, efficient, and visually polished—scalable for both simple overlays and more complex interfaces.