use iced::window::{Id as WindowId, Position};
use iced::{
executor,
widget::{
button, column, container, row, scrollable, text, text_input, Space, horizontal_rule,
},
window, Application, Background, Border, Color, Command, Element, Length, Settings, Shadow,
Theme, Vector,
};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
use wbackend::{Assignment, ExecutionMode, ResourceMode, WBackend};
use crate::parser::{ConfigParser, Protocol, WasmaConfig};
use crate::window_client::WindowClient;
use wsdg_app_manifest::manifest_parser::{CpuCoreServe, ManifestParser};
use wsdg_app_manifest::source_parser::{FileException, PermissionSource, SourceParser};
mod palette {
use iced::Color;
pub const BG_EDITOR: Color = Color { r: 0.117, g: 0.117, b: 0.117, a: 1.0 }; pub const BG_SIDEBAR: Color = Color { r: 0.157, g: 0.157, b: 0.157, a: 1.0 }; pub const BG_ACTIVITY: Color = Color { r: 0.200, g: 0.200, b: 0.200, a: 1.0 }; pub const BG_CARD: Color = Color { r: 0.149, g: 0.149, b: 0.149, a: 1.0 }; pub const BG_CARD_SEL: Color = Color { r: 0.157, g: 0.235, b: 0.349, a: 1.0 }; pub const BG_INPUT: Color = Color { r: 0.227, g: 0.227, b: 0.227, a: 1.0 }; pub const BG_BTN: Color = Color { r: 0.235, g: 0.235, b: 0.235, a: 1.0 }; pub const BG_BTN_HOV: Color = Color { r: 0.275, g: 0.275, b: 0.275, a: 1.0 };
pub const ACCENT_BLUE: Color = Color { r: 0.016, g: 0.478, b: 0.992, a: 1.0 }; pub const ACCENT_TEAL: Color = Color { r: 0.224, g: 0.694, b: 0.624, a: 1.0 }; pub const ACCENT_GREEN: Color = Color { r: 0.224, g: 0.706, b: 0.322, a: 1.0 }; pub const ACCENT_AMBER: Color = Color { r: 0.918, g: 0.631, b: 0.157, a: 1.0 }; pub const ACCENT_RED: Color = Color { r: 0.843, g: 0.290, b: 0.290, a: 1.0 }; pub const ACCENT_GRAY: Color = Color { r: 0.471, g: 0.471, b: 0.471, a: 1.0 };
pub const TEXT_PRIMARY: Color = Color { r: 0.851, g: 0.851, b: 0.851, a: 1.0 }; pub const TEXT_SECONDARY: Color = Color { r: 0.549, g: 0.549, b: 0.549, a: 1.0 }; pub const TEXT_DIM: Color = Color { r: 0.376, g: 0.376, b: 0.376, a: 1.0 };
pub const BORDER_SUBTLE: Color = Color { r: 0.208, g: 0.208, b: 0.208, a: 1.0 }; pub const BORDER_ACTIVE: Color = Color { r: 0.016, g: 0.478, b: 0.992, a: 1.0 }; pub const STATUSBAR_BG: Color = Color { r: 0.012, g: 0.369, b: 0.761, a: 1.0 }; }
#[derive(Debug, Clone, PartialEq)]
pub enum WindowState {
Normal,
Minimized,
Maximized,
Fullscreen,
Hidden,
}
#[derive(Debug, Clone, PartialEq)]
pub enum WindowType {
Normal, Dialog, Utility, Splash,
Menu, Dropdown, Popup, Tooltip, Notification,
}
#[derive(Debug, Clone, Copy)]
pub struct WindowGeometry {
pub x: i32, pub y: i32,
pub width: u32, pub height: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BackendType {
Native, Wayland, X11, Remote(String),
}
#[derive(Debug, Clone)]
pub struct ResourceLimits {
pub max_memory_mb: u64,
pub max_gpu_memory_mb: u64,
pub cpu_cores: Vec<usize>,
pub lease_duration: Duration,
pub execution_mode: Option<ExecutionMode>,
pub renderer: String,
pub pixel_load_limit: u32,
}
impl Default for ResourceLimits {
fn default() -> Self {
Self {
max_memory_mb: 512,
max_gpu_memory_mb: 256,
cpu_cores: Vec::new(),
execution_mode: Some(ExecutionMode::GpuPreferred),
lease_duration: Duration::from_secs(30),
renderer: "cpu_renderer".to_string(),
pixel_load_limit: 50,
}
}
}
#[derive(Debug, Clone)]
pub struct PermissionScope {
pub can_access_network: bool,
pub can_access_filesystem: bool,
pub can_spawn_children: bool,
pub can_use_gpu: bool,
pub allowed_protocols: Vec<String>,
pub sandbox_level: u8,
}
impl Default for PermissionScope {
fn default() -> Self {
Self {
can_access_network: false,
can_access_filesystem: false,
can_spawn_children: false,
can_use_gpu: true,
allowed_protocols: vec!["http".to_string(), "https".to_string()],
sandbox_level: 5,
}
}
}
#[derive(Debug, Clone)]
pub struct Window {
pub id: u64,
pub iced_window_id: Option<WindowId>,
pub title: String,
pub app_id: String,
pub state: WindowState,
pub window_type: WindowType,
pub geometry: WindowGeometry,
pub parent_id: Option<u64>,
pub children_ids: Vec<u64>,
pub visible: bool,
pub focused: bool,
pub resource_limits: ResourceLimits,
pub permissions: PermissionScope,
pub manifest_path: Option<String>,
pub created_at: SystemTime,
pub last_activity: SystemTime,
pub backend_type: BackendType,
pub assignment_id: Option<u32>,
pub resource_mode: ResourceMode,
}
#[derive(Debug, Clone)]
pub struct ResourceUsage {
pub window_id: u64,
pub assignment_id: u32,
pub ram_allocated_mb: u64,
pub vram_allocated_mb: u64,
pub cpu_cores: Vec<usize>,
pub gpu_device: Option<String>,
pub task_active: bool,
pub gpu_active: bool,
pub remaining_lease_secs: u64,
pub execution_mode: ExecutionMode,
}
pub struct WindowHandler {
windows: Arc<Mutex<HashMap<u64, Window>>>,
next_id: Arc<Mutex<u64>>,
focused_window: Arc<Mutex<Option<u64>>>,
wbackend: Arc<WBackend>,
assignment_to_window: Arc<Mutex<HashMap<u32, u64>>>,
wasma_config: Arc<Mutex<Option<WasmaConfig>>>,
}
impl WindowHandler {
pub fn new(resource_mode: ResourceMode) -> Self {
Self {
windows: Arc::new(Mutex::new(HashMap::new())),
next_id: Arc::new(Mutex::new(1)),
focused_window: Arc::new(Mutex::new(None)),
wbackend: Arc::new(WBackend::new(resource_mode)),
assignment_to_window: Arc::new(Mutex::new(HashMap::new())),
wasma_config: Arc::new(Mutex::new(None)),
}
}
pub fn load_wasma_config(&self, config_path: &str) -> Result<(), String> {
let parser = ConfigParser::new(Some(config_path.to_string()));
let config = parser.load().map_err(|e| format!("{:?}", e))?;
parser.validate(&config).map_err(|e| format!("{:?}", e))?;
*self.wasma_config.lock().unwrap() = Some(config);
Ok(())
}
pub fn create_window(
&self,
title: String, app_id: String,
geometry: WindowGeometry,
manifest_path: Option<String>,
resource_mode: ResourceMode,
) -> Result<u64, String> {
let mut next_id = self.next_id.lock().unwrap();
let window_id = *next_id;
*next_id += 1;
let (mut resource_limits, mut permissions) = if let Some(ref path) = manifest_path {
self.load_manifest_and_source(path)?
} else {
(ResourceLimits::default(), PermissionScope::default())
};
if let Some(ref wasma_cfg) = *self.wasma_config.lock().unwrap() {
resource_limits.renderer = wasma_cfg.resource_limits.renderer.clone();
resource_limits.pixel_load_limit = wasma_cfg.resource_limits.scope_level;
for proto_cfg in &wasma_cfg.uri_handling.protocols {
let proto_str = match proto_cfg.protocol {
Protocol::Http => "http", Protocol::Https => "https",
Protocol::Grpc => "grpc", Protocol::Tor => "tor",
};
if !permissions.allowed_protocols.contains(&proto_str.to_string()) {
permissions.allowed_protocols.push(proto_str.to_string());
}
}
}
let assignment_id = window_id as u32;
let mut assignment = Assignment::new(assignment_id);
assignment.execution_mode = resource_limits.execution_mode.unwrap_or(ExecutionMode::GpuPreferred);
assignment.ram_limit = (resource_limits.max_memory_mb * 1024 * 1024) as usize;
assignment.vram_limit = (resource_limits.max_gpu_memory_mb * 1024 * 1024) as usize;
if !resource_limits.cpu_cores.is_empty() {
assignment.cpu_cores = resource_limits.cpu_cores.clone();
}
assignment.start_lease(resource_limits.lease_duration);
self.wbackend.add_assignment(assignment);
self.assignment_to_window.lock().unwrap().insert(assignment_id, window_id);
let window = Window {
id: window_id, iced_window_id: None,
title, app_id, state: WindowState::Normal,
window_type: WindowType::Normal, geometry,
parent_id: None, children_ids: Vec::new(),
visible: true, focused: false,
resource_limits, permissions, manifest_path,
created_at: SystemTime::now(), last_activity: SystemTime::now(),
backend_type: BackendType::Native,
assignment_id: Some(assignment_id), resource_mode,
};
self.windows.lock().unwrap().insert(window_id, window);
Ok(window_id)
}
fn load_manifest_and_source(&self, manifest_path: &str) -> Result<(ResourceLimits, PermissionScope), String> {
let parser = ManifestParser::new(manifest_path.to_string());
let manifest = parser.load().map_err(|e| format!("{:?}", e))?;
let mut limits = ResourceLimits::default();
limits.cpu_cores = match manifest.resources.cpu_core_serve {
CpuCoreServe::Static(n) => (0..n as usize).collect(),
_ => Vec::new(),
};
limits.max_memory_mb = manifest.resources.ram_using.size;
limits.max_gpu_memory_mb = manifest.resources.gpu_using.size;
limits.execution_mode = Some(ExecutionMode::GpuPreferred);
let source_parser = SourceParser::new(None);
let perms = if let Ok(source) =
source_parser.load_embedded(&std::fs::read_to_string(manifest_path).unwrap_or_default())
{
source.map(|s| self.parse_permissions(s)).unwrap_or_default()
} else {
PermissionScope::default()
};
Ok((limits, perms))
}
fn parse_permissions(&self, source: PermissionSource) -> PermissionScope {
PermissionScope {
can_access_network: source.network.ethernet || source.network.wifi,
can_access_filesystem: !matches!(source.filesystem.file_exception, FileException::None),
can_use_gpu: true,
allowed_protocols: vec!["http".to_string(), "https".to_string()],
..Default::default()
}
}
pub fn run_resource_cycle(&self) { self.wbackend.run_cycle(); }
pub fn adjust_window_resources(&self, window_id: u64, new_limits: ResourceLimits) -> Result<(), String> {
let mut windows = self.windows.lock().unwrap();
let window = windows.get_mut(&window_id).ok_or_else(|| format!("Window {} not found", window_id))?;
if window.resource_mode != ResourceMode::Manual {
return Err(format!("Window {} is in Auto mode", window_id));
}
window.resource_limits = new_limits.clone();
if let Some(aid) = window.assignment_id {
if let Some(mut a) = self.wbackend.get_assignment(aid) {
a.execution_mode = new_limits.execution_mode.unwrap_or(ExecutionMode::GpuPreferred);
a.ram_limit = (new_limits.max_memory_mb * 1024 * 1024) as usize;
a.vram_limit = (new_limits.max_gpu_memory_mb * 1024 * 1024) as usize;
}
}
Ok(())
}
pub fn get_window_resource_usage(&self, window_id: u64) -> Result<ResourceUsage, String> {
let windows = self.windows.lock().unwrap();
let window = windows.get(&window_id).ok_or_else(|| format!("Window {} not found", window_id))?;
if let Some(aid) = window.assignment_id {
if let Some(a) = self.wbackend.get_assignment(aid) {
let gpu_active = a.gpu_device.is_some() && !matches!(a.execution_mode, ExecutionMode::CpuOnly);
let remaining = a.lease_duration.and_then(|d| a.lease_start.map(|s| d.as_secs().saturating_sub(s.elapsed().as_secs()))).unwrap_or(0);
return Ok(ResourceUsage {
window_id, assignment_id: aid,
ram_allocated_mb: (a.ram_limit / (1024 * 1024)) as u64,
vram_allocated_mb: (a.vram_limit / (1024 * 1024)) as u64,
cpu_cores: a.cpu_cores.clone(),
gpu_device: a.gpu_device.clone(),
task_active: a.task_handle.is_some(),
gpu_active, remaining_lease_secs: remaining,
execution_mode: a.execution_mode,
});
}
}
Err(format!("Assignment not found for window {}", window_id))
}
pub fn set_window_state(&self, id: u64, state: WindowState) -> Result<(), String> {
let mut w = self.windows.lock().unwrap();
w.get_mut(&id).ok_or_else(|| format!("Window {} not found", id))
.map(|win| { win.state = state; win.last_activity = SystemTime::now(); })
}
pub fn focus_window(&self, id: u64) -> Result<(), String> {
let mut windows = self.windows.lock().unwrap();
for w in windows.values_mut() { w.focused = false; }
if let Some(w) = windows.get_mut(&id) {
w.focused = true; w.last_activity = SystemTime::now();
drop(windows);
*self.focused_window.lock().unwrap() = Some(id);
Ok(())
} else {
Err(format!("Window {} not found", id))
}
}
pub fn close_window(&self, id: u64) -> Result<(), String> {
let mut windows = self.windows.lock().unwrap();
if let Some(w) = windows.get(&id).cloned() {
for child_id in &w.children_ids {
if let Some(c) = windows.get(child_id) {
if let Some(caid) = c.assignment_id {
if let Some(mut a) = self.wbackend.get_assignment(caid) { a.stop_task(); }
}
}
windows.remove(child_id);
}
if let Some(pid) = w.parent_id {
if let Some(p) = windows.get_mut(&pid) { p.children_ids.retain(|&c| c != id); }
}
if let Some(aid) = w.assignment_id {
if let Some(mut a) = self.wbackend.get_assignment(aid) { a.stop_task(); }
self.assignment_to_window.lock().unwrap().remove(&aid);
}
windows.remove(&id);
Ok(())
} else {
Err(format!("Window {} not found", id))
}
}
pub fn set_geometry(&self, id: u64, geometry: WindowGeometry) -> Result<(), String> {
self.windows.lock().unwrap().get_mut(&id)
.ok_or_else(|| format!("Window {} not found", id))
.map(|w| { w.geometry = geometry; w.last_activity = SystemTime::now(); })
}
pub fn set_parent(&self, child_id: u64, parent_id: u64) -> Result<(), String> {
let mut windows = self.windows.lock().unwrap();
if !windows.contains_key(&parent_id) { return Err(format!("Parent {} not found", parent_id)); }
if let Some(c) = windows.get_mut(&child_id) { c.parent_id = Some(parent_id); }
else { return Err(format!("Child {} not found", child_id)); }
if let Some(p) = windows.get_mut(&parent_id) {
if !p.children_ids.contains(&child_id) { p.children_ids.push(child_id); }
}
Ok(())
}
pub fn get_window(&self, id: u64) -> Option<Window> {
self.windows.lock().unwrap().get(&id).cloned()
}
pub fn list_windows(&self) -> Vec<Window> {
self.windows.lock().unwrap().values().cloned().collect()
}
pub fn get_focused_window(&self) -> Option<u64> {
*self.focused_window.lock().unwrap()
}
}
#[derive(Debug, Clone)]
pub enum Message {
AppNameChanged(String),
BackendSelected(crate::backend_selector::Backend),
LaunchApp,
#[cfg(debug_assertions)]
CreateFakeWindow,
KillApp(u64),
CloseWindow(u64),
FocusWindow(u64),
MinimizeWindow(u64),
MaximizeWindow(u64),
ToggleFullscreen(u64),
HideWindow(u64),
SelectWindow(u64),
UpdateResourceCycle,
AdjustResources(u64),
ChangeExecutionMode(u64, ExecutionMode),
}
pub struct WasmaWindowManager {
handler: Arc<WindowHandler>,
window_client: Arc<Mutex<WindowClient>>,
selected_window: Option<u64>,
app_name_input: String,
selected_backend: crate::backend_selector::Backend,
launch_error: Option<String>,
}
fn solid(color: Color) -> container::Appearance {
container::Appearance {
background: Some(Background::Color(color)),
..Default::default()
}
}
fn card_appearance(bg: Color, border_color: Color, border_width: f32) -> container::Appearance {
container::Appearance {
background: Some(Background::Color(bg)),
border: Border {
color: border_color,
width: border_width,
radius: 4.0.into(),
},
..Default::default()
}
}
fn btn_style(bg: Color, fg: Color) -> button::Appearance {
button::Appearance {
background: Some(Background::Color(bg)),
text_color: fg,
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 3.0.into(),
},
shadow: Shadow::default(),
shadow_offset: Vector::ZERO,
}
}
struct DarkBtn;
impl button::StyleSheet for DarkBtn {
type Style = Theme;
fn active(&self, _: &Theme) -> button::Appearance {
btn_style(palette::BG_BTN, palette::TEXT_PRIMARY)
}
fn hovered(&self, _: &Theme) -> button::Appearance {
btn_style(palette::BG_BTN_HOV, palette::TEXT_PRIMARY)
}
fn pressed(&self, _: &Theme) -> button::Appearance {
btn_style(palette::ACCENT_BLUE, Color::WHITE)
}
}
struct AccentBtn;
impl button::StyleSheet for AccentBtn {
type Style = Theme;
fn active(&self, _: &Theme) -> button::Appearance {
btn_style(palette::ACCENT_BLUE, Color::WHITE)
}
fn hovered(&self, _: &Theme) -> button::Appearance {
btn_style(
Color { r: palette::ACCENT_BLUE.r + 0.08, ..palette::ACCENT_BLUE },
Color::WHITE,
)
}
fn pressed(&self, _: &Theme) -> button::Appearance {
btn_style(palette::ACCENT_BLUE, Color::WHITE)
}
}
struct DangerBtn;
impl button::StyleSheet for DangerBtn {
type Style = Theme;
fn active(&self, _: &Theme) -> button::Appearance {
btn_style(
Color { r: 0.5, g: 0.11, b: 0.11, a: 1.0 },
palette::ACCENT_RED,
)
}
fn hovered(&self, _: &Theme) -> button::Appearance {
btn_style(palette::ACCENT_RED, Color::WHITE)
}
fn pressed(&self, _: &Theme) -> button::Appearance {
btn_style(palette::ACCENT_RED, Color::WHITE)
}
}
struct GhostBtn;
impl button::StyleSheet for GhostBtn {
type Style = Theme;
fn active(&self, _: &Theme) -> button::Appearance {
btn_style(Color::TRANSPARENT, palette::TEXT_SECONDARY)
}
fn hovered(&self, _: &Theme) -> button::Appearance {
btn_style(palette::BG_BTN, palette::TEXT_PRIMARY)
}
fn pressed(&self, _: &Theme) -> button::Appearance {
btn_style(palette::BG_BTN_HOV, palette::TEXT_PRIMARY)
}
}
struct DarkInput;
impl text_input::StyleSheet for DarkInput {
type Style = Theme;
fn active(&self, _: &Theme) -> text_input::Appearance {
text_input::Appearance {
background: Background::Color(palette::BG_INPUT),
border: Border {
color: palette::BORDER_SUBTLE,
width: 1.0,
radius: 3.0.into(),
},
icon_color: palette::TEXT_SECONDARY,
}
}
fn focused(&self, style: &Theme) -> text_input::Appearance {
let mut a = self.active(style);
a.border.color = palette::ACCENT_BLUE;
a.border.width = 1.0;
a
}
fn placeholder_color(&self, _: &Theme) -> Color { palette::TEXT_DIM }
fn value_color(&self, _: &Theme) -> Color { palette::TEXT_PRIMARY }
fn disabled_color(&self, _: &Theme) -> Color { palette::TEXT_DIM }
fn selection_color(&self, _: &Theme) -> Color {
Color { r: palette::ACCENT_BLUE.r, g: palette::ACCENT_BLUE.g, b: palette::ACCENT_BLUE.b, a: 0.4 }
}
fn hovered(&self, style: &Theme) -> text_input::Appearance { self.focused(style) }
fn disabled(&self, style: &Theme) -> text_input::Appearance { self.active(style) }
}
impl Application for WasmaWindowManager {
type Executor = executor::Default;
type Message = Message;
type Theme = Theme;
type Flags = ResourceMode;
fn new(flags: Self::Flags) -> (Self, Command<Message>) {
let handler = Arc::new(WindowHandler::new(flags));
let parser = ConfigParser::new(None);
let config = parser.parse(&parser.generate_default_config())
.unwrap_or_else(|_| panic!("Default config parse failed"));
let window_client = Arc::new(Mutex::new(WindowClient::new(config, 1280, 800)));
if let Err(e) = handler.load_wasma_config("/etc/wasma/wasma.in.conf") {
eprintln!("⚠️ WASMA config could not be loaded: {}", e);
}
(
WasmaWindowManager {
handler,
window_client,
selected_window: None,
app_name_input: String::new(),
selected_backend: crate::backend_selector::Backend::Wayland,
launch_error: None,
},
Command::none(),
)
}
fn title(&self) -> String {
String::from("WASMA Window Manager")
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::AppNameChanged(s) => {
self.app_name_input = s;
self.launch_error = None;
}
Message::BackendSelected(b) => { self.selected_backend = b; }
Message::LaunchApp => {
let name = self.app_name_input.trim().to_string();
if name.is_empty() {
self.launch_error = Some("App name cannot be empty.".into());
return Command::none();
}
let geometry = WindowGeometry { x: 100, y: 100, width: 800, height: 600 };
match self.handler.create_window(name.clone(), name.clone(), geometry, None, ResourceMode::Auto) {
Ok(wid) => {
let wc = self.window_client.lock().unwrap();
match wc.launch_app_with_backend(wid, &name, false, Some(self.selected_backend)) {
Ok(_) => {
self.app_name_input.clear();
self.launch_error = None;
self.selected_window = Some(wid);
}
Err(e) => {
drop(wc);
let _ = self.handler.close_window(wid);
self.launch_error = Some(format!("Launch failed: {}", e));
}
}
}
Err(e) => { self.launch_error = Some(format!("Window creation failed: {}", e)); }
}
}
#[cfg(debug_assertions)]
Message::CreateFakeWindow => {
let count = self.handler.list_windows().len() + 1;
let geometry = WindowGeometry { x: 100, y: 100, width: 800, height: 600 };
if let Ok(id) = self.handler.create_window(
format!("Debug Window {}", count), "debug.fake.app".into(),
geometry, None, ResourceMode::Auto,
) {
self.selected_window = Some(id);
}
}
Message::SelectWindow(id) => { self.selected_window = Some(id); }
Message::KillApp(id) => {
let wc = self.window_client.lock().unwrap();
let _ = wc.kill_app(id);
drop(wc);
let _ = self.handler.close_window(id);
if self.selected_window == Some(id) { self.selected_window = None; }
}
Message::CloseWindow(id) => {
let _ = self.handler.close_window(id);
if self.selected_window == Some(id) { self.selected_window = None; }
}
Message::FocusWindow(id) => {
let wc = self.window_client.lock().unwrap();
let _ = wc.set_focus(id);
drop(wc);
let _ = self.handler.set_window_state(id, WindowState::Normal);
self.selected_window = Some(id);
}
Message::MinimizeWindow(id) => {
let wc = self.window_client.lock().unwrap();
let _ = wc.minimize(id);
drop(wc);
let _ = self.handler.set_window_state(id, WindowState::Minimized);
}
Message::MaximizeWindow(id) => {
let wc = self.window_client.lock().unwrap();
let _ = wc.maximize(id);
drop(wc);
let _ = self.handler.set_window_state(id, WindowState::Maximized);
}
Message::ToggleFullscreen(id) => {
let new_state = self.handler.get_window(id)
.map(|w| if w.state == WindowState::Fullscreen { WindowState::Normal } else { WindowState::Fullscreen })
.unwrap_or(WindowState::Normal);
let wc = self.window_client.lock().unwrap();
let _ = wc.toggle_fullscreen(id);
drop(wc);
let _ = self.handler.set_window_state(id, new_state);
}
Message::HideWindow(id) => {
let wc = self.window_client.lock().unwrap();
let _ = wc.hide(id);
drop(wc);
let _ = self.handler.set_window_state(id, WindowState::Hidden);
}
Message::UpdateResourceCycle => {
let wc = self.window_client.lock().unwrap();
wc.reap_dead();
drop(wc);
self.handler.run_resource_cycle();
}
Message::AdjustResources(_) | Message::ChangeExecutionMode(_, _) => {}
}
Command::none()
}
fn view(&self) -> Element<'_, Message> {
let windows = self.handler.list_windows();
let sidebar = self.build_sidebar(&windows);
let main_area = self.build_main_area(&windows);
let body = row![sidebar, main_area].height(Length::Fill);
let status_bar = self.build_status_bar(&windows);
let root = column![
self.build_title_bar(),
body,
status_bar,
]
.height(Length::Fill);
container(root)
.width(Length::Fill)
.height(Length::Fill)
.style(|_: &Theme| solid(palette::BG_EDITOR))
.into()
}
}
impl WasmaWindowManager {
fn build_title_bar(&self) -> Element<'_, Message> {
let title = text("WASMA Window Manager")
.size(13)
.style(palette::TEXT_SECONDARY);
let cycle_btn = button(text("⟳").size(13))
.on_press(Message::UpdateResourceCycle)
.style(iced::theme::Button::Custom(Box::new(GhostBtn)))
.padding([3, 8]);
#[cfg(debug_assertions)]
let debug_btn = button(text("+ Fake").size(12))
.on_press(Message::CreateFakeWindow)
.style(iced::theme::Button::Custom(Box::new(GhostBtn)))
.padding([3, 8]);
let mut bar = row![
Space::with_width(Length::Fixed(16.0)),
title,
Space::with_width(Length::Fill),
cycle_btn,
]
.spacing(4)
.align_items(iced::Alignment::Center)
.padding([6, 8]);
#[cfg(debug_assertions)]
{ bar = bar.push(debug_btn); }
container(bar)
.width(Length::Fill)
.style(|_: &Theme| container::Appearance {
background: Some(Background::Color(palette::BG_ACTIVITY)),
border: Border {
color: palette::BORDER_SUBTLE,
width: 0.0,
radius: 0.0.into(),
},
..Default::default()
})
.into()
}
fn build_sidebar(&self, windows: &[Window]) -> Element<Message> {
let section_label = text("WINDOWS")
.size(11)
.style(palette::TEXT_DIM);
let input = text_input("Enter app name…", &self.app_name_input)
.on_input(Message::AppNameChanged)
.on_submit(Message::LaunchApp)
.size(13)
.padding([6, 10])
.style(iced::theme::TextInput::Custom(Box::new(DarkInput)))
.width(Length::Fill);
let backend_row = {
let backends = vec![
crate::backend_selector::Backend::Wayland,
crate::backend_selector::Backend::X11,
];
let pick = iced::widget::pick_list(
backends,
Some(self.selected_backend),
Message::BackendSelected,
)
.width(Length::Fixed(96.0))
.text_size(12)
.padding([5, 8]);
let launch = button(text("Launch").size(12))
.on_press(Message::LaunchApp)
.style(iced::theme::Button::Custom(Box::new(AccentBtn)))
.padding([5, 12]);
row![pick, Space::with_width(Length::Fill), launch]
.spacing(6)
.align_items(iced::Alignment::Center)
};
let error_row: Element<Message> = if let Some(ref err) = self.launch_error {
text(err.as_str()).size(11).style(palette::ACCENT_RED).into()
} else {
Space::with_height(Length::Fixed(0.0)).into()
};
let launcher_section = column![
row![
Space::with_width(Length::Fixed(12.0)),
section_label,
].padding([10, 0, 4, 0]),
container(
column![input, backend_row, error_row].spacing(6)
).padding([0, 10, 8, 10]),
container(horizontal_rule(1))
.style(|_: &Theme| solid(palette::BORDER_SUBTLE))
.width(Length::Fill),
]
.spacing(0);
let mut list = column![].spacing(0);
if windows.is_empty() {
list = list.push(
container(
text("No windows.\nLaunch an app to get started.")
.size(12)
.style(palette::TEXT_DIM)
)
.padding([20, 16])
);
} else {
for w in windows {
list = list.push(self.build_sidebar_item(w));
}
}
let sidebar_body = column![
launcher_section,
scrollable(list).height(Length::Fill),
]
.height(Length::Fill);
container(sidebar_body)
.width(Length::Fixed(240.0))
.height(Length::Fill)
.style(|_: &Theme| container::Appearance {
background: Some(Background::Color(palette::BG_SIDEBAR)),
border: Border {
color: palette::BORDER_SUBTLE,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
})
.into()
}
fn build_sidebar_item(&self, window: &Window) -> Element<Message> {
let is_selected = self.selected_window == Some(window.id);
let is_fake = window.app_id == "debug.fake.app";
let (state_dot_color, state_char) = match window.state {
WindowState::Normal => (palette::ACCENT_GREEN, "●"),
WindowState::Minimized => (palette::ACCENT_AMBER, "●"),
WindowState::Maximized => (palette::ACCENT_BLUE, "●"),
WindowState::Fullscreen => (palette::ACCENT_TEAL, "●"),
WindowState::Hidden => (palette::ACCENT_GRAY, "●"),
};
let dot = text(state_char).size(10).style(state_dot_color);
let name = text(window.title.as_str())
.size(13)
.style(if is_selected { palette::TEXT_PRIMARY } else { palette::TEXT_SECONDARY });
let id_badge = text(format!("#{}", window.id)).size(10).style(palette::TEXT_DIM);
let item_content = row![
Space::with_width(Length::Fixed(12.0)),
dot,
Space::with_width(Length::Fixed(6.0)),
name,
Space::with_width(Length::Fill),
id_badge,
Space::with_width(Length::Fixed(10.0)),
]
.align_items(iced::Alignment::Center)
.padding([5, 0]);
let bg = if is_selected { palette::BG_CARD_SEL }
else if is_fake { Color { r: 0.18, g: 0.15, b: 0.10, a: 1.0 } }
else { Color::TRANSPARENT };
let border_left = if is_selected { palette::ACCENT_BLUE } else { Color::TRANSPARENT };
button(
container(item_content)
.width(Length::Fill)
.style(move |_: &Theme| container::Appearance {
background: Some(Background::Color(bg)),
border: Border { color: border_left, width: 0.0, radius: 0.0.into() },
..Default::default()
})
)
.on_press(Message::SelectWindow(window.id))
.style(iced::theme::Button::Custom(Box::new(GhostBtn)))
.padding(0)
.width(Length::Fill)
.into()
}
fn build_main_area(&self, windows: &[Window]) -> Element<Message> {
let content: Element<Message> = if let Some(sel_id) = self.selected_window {
if let Some(window) = windows.iter().find(|w| w.id == sel_id) {
self.build_detail_panel(window)
} else {
self.build_empty_state()
}
} else {
self.build_empty_state()
};
container(content)
.width(Length::Fill)
.height(Length::Fill)
.style(|_: &Theme| solid(palette::BG_EDITOR))
.into()
}
fn build_detail_panel(&self, window: &Window) -> Element<Message> {
let is_fake = window.app_id == "debug.fake.app";
let (state_label, state_color) = match window.state {
WindowState::Normal => ("Normal", palette::ACCENT_GREEN),
WindowState::Minimized => ("Minimized", palette::ACCENT_AMBER),
WindowState::Maximized => ("Maximized", palette::ACCENT_BLUE),
WindowState::Fullscreen => ("Fullscreen", palette::ACCENT_TEAL),
WindowState::Hidden => ("Hidden", palette::ACCENT_GRAY),
};
let title_row = row![
text(window.title.as_str()).size(18).style(palette::TEXT_PRIMARY),
Space::with_width(Length::Fill),
container(text(state_label).size(11).style(state_color))
.padding([3, 8])
.style(move |_: &Theme| container::Appearance {
background: Some(Background::Color(Color { a: 0.15, ..state_color })),
border: Border { color: state_color, width: 1.0, radius: 12.0.into() },
..Default::default()
}),
]
.align_items(iced::Alignment::Center)
.spacing(8);
let meta_row = row![
text(format!("#{} · {}", window.id, window.app_id)).size(12).style(palette::TEXT_DIM),
Space::with_width(Length::Fixed(16.0)),
text(format!("{}×{}", window.geometry.width, window.geometry.height)).size(12).style(palette::TEXT_DIM),
Space::with_width(Length::Fixed(16.0)),
text(format!("@({},{})", window.geometry.x, window.geometry.y)).size(12).style(palette::TEXT_DIM),
]
.spacing(0);
let focus_btn = button(text(" Focus ").size(12))
.on_press(Message::FocusWindow(window.id))
.style(iced::theme::Button::Custom(Box::new(DarkBtn)))
.padding([5, 10]);
let min_btn = button(text(" Min ").size(12))
.on_press(Message::MinimizeWindow(window.id))
.style(iced::theme::Button::Custom(Box::new(DarkBtn)))
.padding([5, 10]);
let max_btn = button(text(" Max ").size(12))
.on_press(Message::MaximizeWindow(window.id))
.style(iced::theme::Button::Custom(Box::new(DarkBtn)))
.padding([5, 10]);
let fs_btn = button(text(" FS ").size(12))
.on_press(Message::ToggleFullscreen(window.id))
.style(iced::theme::Button::Custom(Box::new(DarkBtn)))
.padding([5, 10]);
let hide_btn = button(text(" Hide ").size(12))
.on_press(Message::HideWindow(window.id))
.style(iced::theme::Button::Custom(Box::new(DarkBtn)))
.padding([5, 10]);
let close_kill_btn = if is_fake {
button(text(" ✕ Close ").size(12))
.on_press(Message::CloseWindow(window.id))
.style(iced::theme::Button::Custom(Box::new(DangerBtn)))
.padding([5, 10])
} else {
button(text(" ✕ Kill ").size(12))
.on_press(Message::KillApp(window.id))
.style(iced::theme::Button::Custom(Box::new(DangerBtn)))
.padding([5, 10])
};
let ctrl_row = row![
focus_btn, min_btn, max_btn, fs_btn, hide_btn,
Space::with_width(Length::Fill),
close_kill_btn,
]
.spacing(6)
.align_items(iced::Alignment::Center);
let resource_section = self.build_resource_card(window);
let thumbnail = container(
column![
Space::with_height(Length::Fill),
container(
text(format!("[ {} ]", window.title)).size(14).style(palette::TEXT_DIM)
)
.center_x()
.center_y()
.width(Length::Fill),
Space::with_height(Length::Fill),
]
)
.width(Length::Fill)
.height(Length::Fixed(180.0))
.style(|_: &Theme| container::Appearance {
background: Some(Background::Color(Color { r: 0.08, g: 0.08, b: 0.08, a: 1.0 })),
border: Border { color: palette::BORDER_SUBTLE, width: 1.0, radius: 4.0.into() },
..Default::default()
});
let panel = column![
title_row,
meta_row,
Space::with_height(Length::Fixed(12.0)),
ctrl_row,
Space::with_height(Length::Fixed(16.0)),
thumbnail,
Space::with_height(Length::Fixed(16.0)),
resource_section,
]
.spacing(6)
.padding(24);
container(scrollable(panel))
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn build_resource_card(&self, window: &Window) -> Element<Message> {
let section_title = text("RESOURCE USAGE")
.size(11)
.style(palette::TEXT_DIM);
let inner: Element<Message> = if let Ok(usage) = self.handler.get_window_resource_usage(window.id) {
let mode_str = match usage.execution_mode {
ExecutionMode::CpuOnly => "CPU Only",
ExecutionMode::GpuPreferred => "GPU Preferred",
ExecutionMode::GpuOnly => "GPU Only",
ExecutionMode::Hybrid => "Hybrid",
};
let task_color = if usage.task_active { palette::ACCENT_GREEN } else { palette::ACCENT_AMBER };
let task_str = if usage.task_active { "RUNNING" } else { "STOPPED" };
let gpu_str = if usage.gpu_active { "GPU active" } else { "GPU idle" };
column![
row![
text(format!("Assignment #{}", usage.assignment_id)).size(12).style(palette::TEXT_SECONDARY),
Space::with_width(Length::Fill),
text(mode_str).size(12).style(palette::ACCENT_BLUE),
Space::with_width(Length::Fixed(12.0)),
text(task_str).size(12).style(task_color),
]
.align_items(iced::Alignment::Center),
Space::with_height(Length::Fixed(8.0)),
self.build_resource_bar(
"RAM", usage.ram_allocated_mb,
window.resource_limits.max_memory_mb,
palette::ACCENT_TEAL,
),
Space::with_height(Length::Fixed(6.0)),
self.build_resource_bar(
"VRAM", usage.vram_allocated_mb,
window.resource_limits.max_gpu_memory_mb,
palette::ACCENT_BLUE,
),
Space::with_height(Length::Fixed(8.0)),
row![
text(format!("Renderer: {}", window.resource_limits.renderer)).size(11).style(palette::TEXT_DIM),
Space::with_width(Length::Fill),
text(gpu_str).size(11).style(palette::TEXT_DIM),
],
row![
text(format!("CPU Cores: {:?}", usage.cpu_cores)).size(11).style(palette::TEXT_DIM),
Space::with_width(Length::Fill),
if usage.remaining_lease_secs > 0 {
text(format!("Lease: {}s", usage.remaining_lease_secs)).size(11).style(palette::ACCENT_AMBER)
} else {
text("Lease: ∞").size(11).style(palette::TEXT_DIM)
},
],
]
.spacing(4)
.into()
} else {
text("No resource data available").size(12).style(palette::TEXT_DIM).into()
};
container(
column![
section_title,
Space::with_height(Length::Fixed(8.0)),
inner,
]
.spacing(0)
)
.padding(14)
.width(Length::Fill)
.style(|_: &Theme| card_appearance(palette::BG_CARD, palette::BORDER_SUBTLE, 1.0))
.into()
}
fn build_resource_bar<'a>(
&'a self,
label: &'static str,
used: u64,
total: u64,
bar_color: Color,
) -> Element<'a, Message> {
let ratio = if total > 0 { (used as f32 / total as f32).clamp(0.0, 1.0) } else { 0.0 };
let bar_width = 340.0_f32;
let filled = bar_width * ratio;
let bar = row![
container(Space::with_width(Length::Fixed(filled)))
.height(Length::Fixed(6.0))
.style(move |_: &Theme| container::Appearance {
background: Some(Background::Color(bar_color)),
border: Border { radius: 3.0.into(), ..Default::default() },
..Default::default()
}),
container(Space::with_width(Length::Fixed(bar_width - filled)))
.height(Length::Fixed(6.0))
.style(|_: &Theme| container::Appearance {
background: Some(Background::Color(palette::BG_INPUT)),
border: Border { radius: 3.0.into(), ..Default::default() },
..Default::default()
}),
]
.spacing(0);
row![
text(label).size(11).style(palette::TEXT_SECONDARY).width(Length::Fixed(36.0)),
Space::with_width(Length::Fixed(8.0)),
bar,
Space::with_width(Length::Fixed(8.0)),
text(format!("{}/{} MiB", used, total)).size(11).style(palette::TEXT_DIM),
]
.align_items(iced::Alignment::Center)
.into()
}
fn build_empty_state(&self) -> Element<'_, Message> {
container(
column![
text("WASMA").size(32).style(palette::TEXT_DIM),
Space::with_height(Length::Fixed(8.0)),
text("Select a window from the left panel\nor launch a new app.")
.size(13)
.style(palette::TEXT_DIM),
]
.spacing(4)
.align_items(iced::Alignment::Center)
)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
}
fn build_status_bar(&self, windows: &[Window]) -> Element<Message> {
let win_count = windows.len();
let focused_name = windows.iter()
.find(|w| w.focused)
.map(|w| w.title.as_str())
.unwrap_or("—");
let backend_label = match self.selected_backend {
crate::backend_selector::Backend::Wayland => "⬡ Wayland",
crate::backend_selector::Backend::X11 => "⬡ X11",
};
let bar = row![
Space::with_width(Length::Fixed(12.0)),
text(backend_label).size(11).style(Color::WHITE),
Space::with_width(Length::Fixed(16.0)),
text(format!("Windows: {}", win_count)).size(11).style(Color::WHITE),
Space::with_width(Length::Fill),
text(format!("Focus: {}", focused_name)).size(11).style(Color::WHITE),
Space::with_width(Length::Fixed(12.0)),
text("WASMA v1.3-beta").size(11).style(Color { a: 0.7, ..Color::WHITE }),
Space::with_width(Length::Fixed(12.0)),
]
.align_items(iced::Alignment::Center)
.padding([4, 0]);
container(bar)
.width(Length::Fill)
.height(Length::Fixed(22.0))
.style(|_: &Theme| solid(palette::STATUSBAR_BG))
.into()
}
}
pub fn launch_window_manager(resource_mode: ResourceMode) -> iced::Result {
WasmaWindowManager::run(Settings {
window: window::Settings {
size: [1280.0, 800.0].into(),
position: Position::Centered,
..Default::default()
},
flags: resource_mode,
fonts: vec![],
antialiasing: true,
default_font: Default::default(),
default_text_size: iced::Pixels(14.0),
id: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn make_handler() -> WindowHandler { WindowHandler::new(ResourceMode::Auto) }
fn make_geo() -> WindowGeometry { WindowGeometry { x: 0, y: 0, width: 800, height: 600 } }
#[test]
fn test_window_creation() {
let h = make_handler();
let id = h.create_window("Test".into(), "t.app".into(), make_geo(), None, ResourceMode::Auto).unwrap();
assert!(h.get_window(id).is_some());
}
#[test]
fn test_window_lifecycle() {
let h = make_handler();
let id = h.create_window("Test".into(), "t.app".into(), make_geo(), None, ResourceMode::Auto).unwrap();
h.focus_window(id).unwrap();
assert_eq!(h.get_focused_window(), Some(id));
h.close_window(id).unwrap();
assert!(h.get_window(id).is_none());
}
#[test]
fn test_resource_adjustment() {
let h = WindowHandler::new(ResourceMode::Manual);
let id = h.create_window("R".into(), "r.app".into(), make_geo(), None, ResourceMode::Manual).unwrap();
let mut limits = ResourceLimits::default();
limits.max_memory_mb = 1024;
h.adjust_window_resources(id, limits).unwrap();
assert_eq!(h.get_window(id).unwrap().resource_limits.max_memory_mb, 1024);
}
#[test]
fn test_parent_child() {
let h = make_handler();
let p = h.create_window("P".into(), "p.app".into(), make_geo(), None, ResourceMode::Auto).unwrap();
let c = h.create_window("C".into(), "c.app".into(), make_geo(), None, ResourceMode::Auto).unwrap();
h.set_parent(c, p).unwrap();
assert!(h.get_window(p).unwrap().children_ids.contains(&c));
assert_eq!(h.get_window(c).unwrap().parent_id, Some(p));
}
}