git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commitd7604d5b6765cb9c4fe1e35eac1b13cf7314321c
parent8d289c380f
authorLucas Galante <[email protected]>
date2026-06-01 11:49
Refactor system-interface to use clear-ui's strongly-typed LogicalPosition and LogicalSize

 src/main.rs         | 975 +++++++++-------------------------------------------
 src/pages/colors.rs |   2 +-
 src/pages/layout.rs | 203 ++++++++++-
 3 files changed, 375 insertions(+), 805 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index b844821..6c3fc8f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,7 @@
 use std::sync::Arc;
 
 use clear_ui::color;
-use clear_ui::widget::{Spinbox, Widget, Finger, Slider, hover_animation};
+use clear_ui::widget::{Spinbox, Widget, Finger, Slider, hover_animation, TextItem};
 use glyphon::{
     Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
     TextBounds, TextRenderer, Viewport,
@@ -169,38 +169,18 @@ enum WidgetKind {
     Static,
 }
 
-struct TextItem {
-    buffer: Buffer,
-    x: f32, y: f32,
-    color: glyphon::Color,
-}
-
 enum ColorSelectorAction {
     Background([u8; 3]),
     Border([u8; 3]),
 }
 
-struct SystemInterface {
-    window: XdgWindow,
-    surface: wl_surface::WlSurface,
-    wgpu_surface: wgpu::Surface<'static>,
-    device: wgpu::Device,
-    queue: wgpu::Queue,
-    config: wgpu::SurfaceConfiguration,
-    render_pipeline: wgpu::RenderPipeline,
-    vertex_buffer: wgpu::Buffer,
-    vertex_count: u32,
+static INITIAL_PAGE_INDEX: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
 
+struct SystemInterface {
     app: AppState,
-
     font_system: FontSystem,
-    swash_cache: SwashCache,
-    text_atlas: TextAtlas,
-    text_renderer: TextRenderer,
-    text_viewport: Viewport,
-
     widgets: Vec<AppWidget>,
-    text_items: Vec<TextItem>,
+    text_items: Vec<clear_ui::widget::TextItem>,
     page_buttons: Vec<ContentButton>,
 
     sidebar_width: f32,
@@ -243,109 +223,13 @@ struct SystemInterface {
     sans_serif_family: String,
     serif_family: String,
     monospace_family: String,
+    sender: calloop::channel::Sender<AppAction>,
 }
 
-impl SystemInterface {
-    async fn new(
-        conn: &Connection,
-        qh: &QueueHandle<App>,
-        compositor_state: &CompositorState,
-        xdg_shell_state: &XdgShell,
-        width: u32,
-        height: u32,
-        scale: f64,
-    ) -> Self {
-        let surface = compositor_state.create_surface(qh);
-        surface.set_buffer_scale(scale as i32);
-        let window = xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, qh);
-        window.set_title("Clear System Interface");
-        window.set_app_id("clear-system-interface");
-        window.set_min_size(Some((820, 680)));
-        window.commit();
-
-        let wayland_handle = Box::leak(Box::new(clear_ui::wayland::WaylandSurfaceHandle {
-            display_ptr: conn.backend().display_id().as_ptr() as *mut std::ffi::c_void,
-            surface_ptr: surface.id().as_ptr() as *mut std::ffi::c_void,
-        }));
-
-        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
-            backends: wgpu::Backends::VULKAN,
-            ..Default::default()
-        });
-        let wgpu_surface = instance.create_surface(wayland_handle).expect("surface");
-        let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
-            power_preference: wgpu::PowerPreference::LowPower,
-            compatible_surface: Some(&wgpu_surface),
-            force_fallback_adapter: false,
-        }).await.expect("adapter");
-        let (device, queue) = adapter.request_device(&wgpu::DeviceDescriptor {
-            label: Some("GPU Device"),
-            required_features: wgpu::Features::empty(),
-            required_limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()),
-            memory_hints: wgpu::MemoryHints::MemoryUsage,
-        }, None).await.expect("device");
-        let config = wgpu_surface.get_default_config(&adapter, width.max(1), height.max(1)).expect("config");
-        wgpu_surface.configure(&device, &config);
-
-        let shader_code = clear_ui::SHADER;
-        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
-            label: Some("Shader"),
-            source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(shader_code)),
-        });
-        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
-            label: Some("Pipeline Layout"),
-            bind_group_layouts: &[],
-            push_constant_ranges: &[],
-        });
-        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
-            label: Some("Render Pipeline"),
-            layout: Some(&pipeline_layout),
-            vertex: wgpu::VertexState {
-                module: &shader,
-                entry_point: Some("vs_main"),
-                buffers: &[Vertex::desc()],
-                compilation_options: Default::default(),
-            },
-            fragment: Some(wgpu::FragmentState {
-                module: &shader,
-                entry_point: Some("fs_main"),
-                targets: &[Some(wgpu::ColorTargetState {
-                    format: config.format,
-                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
-                    write_mask: wgpu::ColorWrites::ALL,
-                })],
-                compilation_options: Default::default(),
-            }),
-            primitive: wgpu::PrimitiveState {
-                topology: wgpu::PrimitiveTopology::TriangleList,
-                front_face: wgpu::FrontFace::Ccw,
-                cull_mode: None,
-                polygon_mode: wgpu::PolygonMode::Fill,
-                unclipped_depth: false,
-                conservative: false,
-                strip_index_format: None,
-            },
-            depth_stencil: None,
-            multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false },
-            multiview: None,
-            cache: None,
-        });
-
-        let font_system = FontSystem::new();
-        let swash_cache = SwashCache::new();
-        let cache = Cache::new(&device);
-        let mut text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
-        let text_renderer = TextRenderer::new(&mut text_atlas, &device, wgpu::MultisampleState::default(), None);
-        let mut text_viewport = Viewport::new(&device, &cache);
-        text_viewport.update(&queue, Resolution { width, height });
-
-        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
-            label: Some("Vertex Buffer"),
-            size: 1,
-            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
-            mapped_at_creation: false,
-        });
+impl clear_ui::engine::Application for SystemInterface {
+    type Message = AppAction;
 
+    fn new(_qh: &wayland_client::QueueHandle<clear_ui::engine::EngineState<Self>>, sender: calloop::channel::Sender<Self::Message>) -> Self {
         let app = AppState {
             layout: pages::layout::read_layout_config(),
             input: pages::input::read_input_config(),
@@ -446,29 +330,52 @@ impl SystemInterface {
             rx
         };
         let (tx_backup, rx_backup) = std::sync::mpsc::channel();
-
         let (tx_color_selector, rx_color_selector) = std::sync::mpsc::channel();
-        let scale_factor = scale;
 
         let (sans_family, serif_family, monospace_family, _, _, _, _) = pages::typeface::read_preferred_fonts();
 
         let pages_names = Page::ALL.iter().map(|p| p.label().to_string()).collect::<Vec<_>>();
         let paginator = clear_ui::widget::Paginator::new(140.0, pages_names);
 
+        let initial_page_idx = INITIAL_PAGE_INDEX.load(std::sync::atomic::Ordering::SeqCst);
+        let mut app_state = app;
+        app_state.current_page = Page::ALL[initial_page_idx];
+
+        let font_system = FontSystem::new();
+
         let mut this = Self {
-            window, surface, wgpu_surface, device, queue, config, render_pipeline,
-            vertex_buffer, vertex_count: 0,
-            app,
-            font_system, swash_cache, text_atlas, text_renderer, text_viewport,
-            widgets: Vec::new(), text_items: Vec::new(), page_buttons: Vec::new(),
-            sidebar_width: 140.0, header_height: 0.0, status_height: 0.0,
-            cursor_x: 0.0, cursor_y: 0.0,
-            scale_factor,
-            rx_audio, rx_display, rx_network, rx_layout, rx_input, rx_fingers,
-            rx_hardware, rx_system, rx_status, rx_storage, rx_notifications,
-            rx_backup_state, rx_typeface, rx_services, rx_colors, tx_backup, rx_backup,
-            tx_color_selector, rx_color_selector,
-            width, height,
+            app: app_state,
+            font_system,
+            widgets: Vec::new(),
+            text_items: Vec::new(),
+            page_buttons: Vec::new(),
+            sidebar_width: 140.0,
+            header_height: 0.0,
+            status_height: 0.0,
+            cursor_x: 0.0,
+            cursor_y: 0.0,
+            rx_audio,
+            rx_display,
+            rx_network,
+            rx_layout,
+            rx_input,
+            rx_fingers,
+            rx_hardware,
+            rx_system,
+            rx_status,
+            rx_storage,
+            rx_notifications,
+            rx_backup_state,
+            rx_typeface,
+            rx_services,
+            rx_colors,
+            tx_backup,
+            rx_backup,
+            tx_color_selector,
+            rx_color_selector,
+            scale_factor: 1.0,
+            width: 820,
+            height: 680,
             needs_rebuild: true,
             scroll_y: 0.0,
             max_scroll_y: 0.0,
@@ -479,11 +386,95 @@ impl SystemInterface {
             sans_serif_family: sans_family,
             serif_family,
             monospace_family,
+            sender,
         };
-        this.rebuild_layout(width as f32, height as f32);
+        this.rebuild_layout(820.0, 680.0);
         this
     }
 
+    fn settings(&self) -> clear_ui::engine::WindowSettings {
+        clear_ui::engine::WindowSettings {
+            title: "Clear System Interface".to_string(),
+            app_id: "clear-system-interface".to_string(),
+            width: 820,
+            height: 680,
+            fullscreen: false,
+            min_size: Some((820, 680)),
+        }
+    }
+
+    fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, _exit: &mut bool) {
+        self.handle_action(&msg);
+        *needs_rebuild = true;
+        self.needs_rebuild = true;
+    }
+
+    fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
+        self.poll_background_updates();
+        if self.tick_internal(dt) {
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
+        }
+        if self.needs_rebuild {
+            *needs_rebuild = true;
+        }
+    }
+
+    fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: clear_ui::engine::LogicalSize, scale: f64) {
+        let (width, height) = (size.width, size.height);
+        if self.needs_rebuild || self.width != width as u32 || self.height != height as u32 || self.scale_factor != scale {
+            self.width = width as u32;
+            self.height = height as u32;
+            self.scale_factor = scale;
+            self.rebuild_layout(width, height);
+        }
+        for w in &self.widgets {
+            quads.push((w.x, w.y, w.w, w.h, w.color));
+        }
+    }
+
+    fn text_items(&self) -> &[clear_ui::widget::TextItem] {
+        &self.text_items
+    }
+
+    fn clear_color(&self) -> [f32; 4] {
+        [
+            self.app.colors.page_low_color[0] as f32 / 255.0,
+            self.app.colors.page_low_color[1] as f32 / 255.0,
+            self.app.colors.page_low_color[2] as f32 / 255.0,
+            1.0,
+        ]
+    }
+
+    fn handle_pointer_move(&mut self, pos: clear_ui::engine::LogicalPosition, needs_rebuild: &mut bool) {
+        if self.handle_cursor_moved(pos.x, pos.y) {
+            *needs_rebuild = true;
+        }
+    }
+
+    fn handle_mouse_input(&mut self, button: clear_ui::widget::MouseButton, state: clear_ui::widget::ElementState, _pos: clear_ui::engine::LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message> {
+        if self.handle_mouse_input_internal(button, state) {
+            *needs_rebuild = true;
+        }
+        None
+    }
+
+    fn handle_mouse_wheel(&mut self, delta: &clear_ui::widget::MouseScrollDelta, _pos: clear_ui::engine::LogicalPosition, needs_rebuild: &mut bool) {
+        if self.handle_mouse_wheel_internal(delta) {
+            *needs_rebuild = true;
+        }
+    }
+
+    fn handle_key_input(&mut self, event: &clear_ui::widget::KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message> {
+        if self.handle_key_input_internal(event) {
+            *needs_rebuild = true;
+        }
+        None
+    }
+}
+
+impl SystemInterface {
+
 fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f32, f32, f32, f32)>) {
     if let Some(rect) = w.popover_rect() {
         popovers.push(rect);
@@ -498,7 +489,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
 }
 
     fn rebuild_layout(&mut self, sw: f32, sh: f32) {
-        let s = self.scale_factor as f32;
+        let s = 1.0f32;
         let mut widgets = Vec::new();
         let mut text_items = Vec::new();
         let mut page_buttons = Vec::new();
@@ -724,6 +715,11 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
         self.app.display.brightness_spinbox.clear_children(); self.app.display.brightness_spinbox.set_parent(None);
         self.app.status.padding_spinbox.clear_children(); self.app.status.padding_spinbox.set_parent(None);
 
+        for menu in &mut self.app.layout.tag_layout_menus {
+            menu.clear_children();
+            menu.set_parent(None);
+        }
+
         use clear_ui::widget::focus::link_parent_child;
         match self.app.current_page {
             Page::Typefaces => {
@@ -760,9 +756,9 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 link_parent_child(&mut self.page_root_container, &mut self.app.network.wifi_list_box.scroll_box);
             }
             Page::Layout => {
-                self.page_sec_containers.resize_with(3, clear_ui::widget::Container::new);
+                self.page_sec_containers.resize_with(4, clear_ui::widget::Container::new);
                 
-                for i in 0..3 {
+                for i in 0..4 {
                     link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[i]);
                 }
                 
@@ -775,6 +771,10 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 link_parent_child(&mut self.page_sec_containers[1], &mut self.app.layout.top_gap_spinbox);
                 
                 link_parent_child(&mut self.page_sec_containers[2], &mut self.app.layout.transition_duration_spinbox);
+
+                for menu in &mut self.app.layout.tag_layout_menus {
+                    link_parent_child(&mut self.page_sec_containers[3], menu);
+                }
             }
             Page::Colors => {
                 for cs in &mut self.app.colors.color_selectors {
@@ -919,64 +919,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
         }
     }
 
-    fn collect_vertices(&self) -> Vec<Vertex> {
-        let sw = self.width as f32;
-        let sh = self.height as f32;
-        let mut verts = Vec::new();
-        for w in &self.widgets {
-            verts.extend(quad_vertices(w.x, w.y, w.w, w.h, sw, sh, w.color));
-        }
-        verts
-    }
 
-    fn upload_vertices(&mut self) {
-        let verts = self.collect_vertices();
-        self.vertex_count = verts.len() as u32;
-        let data = bytemuck::cast_slice(&verts);
-        let needed = data.len() as wgpu::BufferAddress;
-        if needed > self.vertex_buffer.size() {
-            self.vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
-                label: Some("Vertex Buffer"),
-                size: needed,
-                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
-                mapped_at_creation: false,
-            });
-        }
-        self.queue.write_buffer(&self.vertex_buffer, 0, data);
-    }
-
-    fn prepare_text(&mut self) {
-        let Self {
-            ref mut text_renderer, ref device, ref queue,
-            ref mut font_system, ref mut text_atlas,
-            ref mut text_viewport, ref mut swash_cache,
-            ref text_items, width, height, ..
-        } = self;
-
-        let w = *width as f32;
-        let h = *height as f32;
-        let viewport = Resolution { width: w as u32, height: h as u32 };
-        text_viewport.update(queue, viewport);
-        let bounds = TextBounds { left: 0, top: 0, right: w as i32, bottom: h as i32 };
-        let areas: Vec<TextArea> = text_items.iter().map(|ti| TextArea {
-            buffer: &ti.buffer,
-            left: ti.x, top: ti.y, scale: 1.0, bounds,
-            default_color: ti.color,
-            custom_glyphs: &[],
-        }).collect();
-        text_renderer.prepare(device, queue, font_system, text_atlas, text_viewport, areas, swash_cache).unwrap();
-    }
-
-    fn resize(&mut self, width: u32, height: u32) {
-        if width > 0 && height > 0 {
-            self.width = width; self.height = height;
-            self.config.width = width; self.config.height = height;
-            self.wgpu_surface.configure(&self.device, &self.config);
-            self.needs_rebuild = true;
-        }
-    }
-
-    fn tick(&mut self, dt: f32) -> bool {
+    fn tick_internal(&mut self, dt: f32) -> bool {
         let mut needs_redraw = false;
         if hover_animation::tick(dt) {
             needs_redraw = true;
@@ -1121,7 +1065,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
     fn handle_cursor_moved(&mut self, x: f32, y: f32) -> bool {
         self.cursor_x = x;
         self.cursor_y = y;
-        let s = self.scale_factor as f32;
+        let s = 1.0f32;
         let lx_no_scroll = x / s;
         let ly_no_scroll = y / s;
         
@@ -1167,6 +1111,11 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
             if self.app.layout.transition_duration_spinbox.cursor_moved(lx, ly) {
                 changed = true;
             }
+            for menu in &mut self.app.layout.tag_layout_menus {
+                if menu.cursor_moved(lx, ly) {
+                    changed = true;
+                }
+            }
         }
         if self.app.current_page == Page::Colors {
             for cp in &mut self.app.colors.color_selectors {
@@ -1371,8 +1320,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
         changed
     }
 
-    fn handle_mouse_input(&mut self, button: clear_ui::widget::MouseButton, state: clear_ui::widget::ElementState) -> bool {
-        let s = self.scale_factor as f32;
+    fn handle_mouse_input_internal(&mut self, button: clear_ui::widget::MouseButton, state: clear_ui::widget::ElementState) -> bool {
+        let s = 1.0f32;
         let lx_no_scroll = self.cursor_x / s;
         let ly_no_scroll = self.cursor_y / s;
 
@@ -1422,6 +1371,9 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                     if self.app.layout.edge_gap_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
                     if self.app.layout.top_gap_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
                     if self.app.layout.transition_duration_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+                    for menu in &mut self.app.layout.tag_layout_menus {
+                        if menu.hit_test(lx, ly) { clicked_any_focusable = true; }
+                    }
                 }
                 Page::Colors => {
                     for cp in &mut self.app.colors.color_selectors {
@@ -1548,6 +1500,17 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 ));
             }
         }
+        if self.app.current_page == Page::Layout {
+            for (idx, menu) in self.app.layout.tag_layout_menus.iter_mut().enumerate() {
+                if state == clear_ui::widget::ElementState::Pressed && !menu.hit_test(lx, ly) { menu.unfocus(); }
+                if menu.mouse_input(button, state, lx, ly) {
+                    self.needs_rebuild = true;
+                }
+                if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
+                    actions.push(AppAction::Layout(pages::layout::LayoutMessage::SetTagLayout(idx + 1, menu.selected)));
+                }
+            }
+        }
         if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Colors {
             for (i, cp) in self.app.colors.color_selectors.iter_mut().enumerate() {
                 let old = cp.color;
@@ -2004,8 +1967,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
         true
     }
 
-    fn handle_mouse_wheel(&mut self, delta: &clear_ui::widget::MouseScrollDelta) -> bool {
-        let s = self.scale_factor as f32;
+    fn handle_mouse_wheel_internal(&mut self, delta: &clear_ui::widget::MouseScrollDelta) -> bool {
+        let s = 1.0f32;
         if self.cursor_x >= self.sidebar_width * s {
             let lx = self.cursor_x / s;
             let ly = self.cursor_y / s + self.scroll_y;
@@ -2076,7 +2039,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
         }
     }
 
-    fn handle_key_input(&mut self, event: &clear_ui::widget::KeyEvent) -> bool {
+    fn handle_key_input_internal(&mut self, event: &clear_ui::widget::KeyEvent) -> bool {
         if event.state == clear_ui::widget::ElementState::Pressed && !event.repeat {
             let is_nav_key = match (&event.logical_key, event.ctrl) {
                 (clear_ui::widget::Key::Character(c), true) if c == "j" || c == "J" || c == "k" || c == "K" || c == "u" || c == "U" || c == "i" || c == "I" => true,
@@ -2527,494 +2490,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
         false
     }
 
-    fn render(&mut self) {
-        self.poll_background_updates();
-
-        let sw = self.width as f32;
-        let sh = self.height as f32;
-
-        if self.needs_rebuild {
-            self.rebuild_layout(sw, sh);
-            self.upload_vertices();
-        }
-
-        self.prepare_text();
-
-        let output = match self.wgpu_surface.get_current_texture() {
-            Ok(t) => t,
-            Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
-                self.wgpu_surface.configure(&self.device, &self.config);
-                return;
-            }
-            Err(wgpu::SurfaceError::Timeout) => return,
-            Err(e) => { eprintln!("Surface error: {e:?}"); return; }
-        };
-
-        let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
-        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
-            label: Some("Encoder"),
-        });
-
-        {
-            let r_clear = (self.app.colors.page_low_color[0] as f64 / 255.0).powf(2.2);
-            let g_clear = (self.app.colors.page_low_color[1] as f64 / 255.0).powf(2.2);
-            let b_clear = (self.app.colors.page_low_color[2] as f64 / 255.0).powf(2.2);
-
-            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                label: Some("Render Pass"),
-                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                    view: &view,
-                    resolve_target: None,
-                    ops: wgpu::Operations {
-                        load: wgpu::LoadOp::Clear(wgpu::Color {
-                            r: r_clear,
-                            g: g_clear,
-                            b: b_clear,
-                            a: 1.0,
-                        }),
-                        store: wgpu::StoreOp::Store,
-                    },
-                })],
-                depth_stencil_attachment: None,
-                timestamp_writes: None,
-                occlusion_query_set: None,
-            });
-
-            pass.set_pipeline(&self.render_pipeline);
-            pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
-            pass.draw(0..self.vertex_count, 0..1);
-
-            self.text_renderer.render(&self.text_atlas, &self.text_viewport, &mut pass).unwrap();
-        }
-
-        self.queue.submit(std::iter::once(encoder.finish()));
-        output.present();
-    }
 }
 
-struct PressedKey {
-    logical_key: clear_ui::widget::Key,
-    text: Option<String>,
-    first_pressed: std::time::Instant,
-    last_repeated: std::time::Instant,
-}
-
-fn is_repeatable_key(key: &clear_ui::widget::Key) -> bool {
-    use clear_ui::widget::{Key, NamedKey};
-    match key {
-        Key::Named(NamedKey::Backspace) |
-        Key::Named(NamedKey::Delete) |
-        Key::Named(NamedKey::ArrowLeft) |
-        Key::Named(NamedKey::ArrowRight) |
-        Key::Named(NamedKey::ArrowUp) |
-        Key::Named(NamedKey::ArrowDown) |
-        Key::Named(NamedKey::Home) |
-        Key::Named(NamedKey::End) |
-        Key::Character(_) => true,
-        _ => false,
-    }
-}
-
-struct App {
-    registry_state: RegistryState,
-    compositor_state: CompositorState,
-    xdg_shell_state: XdgShell,
-    shm_state: Shm,
-    seat_state: SeatState,
-    output_state: OutputState,
-
-    seats: Vec<wl_seat::WlSeat>,
-    pointer: Option<ThemedPointer>,
-    keyboard: Option<wl_keyboard::WlKeyboard>,
-
-    window: Option<XdgWindow>,
-    surface: Option<wl_surface::WlSurface>,
-
-    state: Option<SystemInterface>,
-    exit: bool,
-    redraw: bool,
-    ctrl_pressed: bool,
-    shift_pressed: bool,
-    pressed_key: Option<PressedKey>,
-}
-
-
-
-impl CompositorHandler for App {
-    fn scale_factor_changed(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        scale_factor: i32,
-    ) {
-        _surface.set_buffer_scale(scale_factor);
-        if let Some(state) = &mut self.state {
-            let old_scale = state.scale_factor;
-            state.scale_factor = scale_factor as f64;
-            let logical_w = state.width as f64 / old_scale;
-            let logical_h = state.height as f64 / old_scale;
-            let pw = (logical_w * state.scale_factor) as u32;
-            let ph = (logical_h * state.scale_factor) as u32;
-            state.resize(pw, ph);
-        }
-        self.redraw = true;
-    }
-
-    fn transform_changed(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        _new_transform: wl_output::Transform,
-    ) {}
-
-    fn frame(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        _time: u32,
-    ) {}
-
-    fn surface_enter(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        _output: &wl_output::WlOutput,
-    ) {}
-
-    fn surface_leave(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        _output: &wl_output::WlOutput,
-    ) {}
-}
-
-impl OutputHandler for App {
-    fn output_state(&mut self) -> &mut OutputState {
-        &mut self.output_state
-    }
-
-    fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
-    fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
-    fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
-}
-
-impl SeatHandler for App {
-    fn seat_state(&mut self) -> &mut SeatState {
-        &mut self.seat_state
-    }
-
-    fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
-        self.seats.push(seat);
-    }
-
-    fn new_capability(
-        &mut self,
-        _conn: &Connection,
-        qh: &QueueHandle<Self>,
-        seat: wl_seat::WlSeat,
-        capability: Capability,
-    ) {
-        if capability == Capability::Pointer && self.pointer.is_none() {
-            let surface = self.compositor_state.create_surface(qh);
-            let themed_pointer = self.seat_state.get_pointer_with_theme(
-                qh,
-                &seat,
-                self.shm_state.wl_shm(),
-                surface,
-                ThemeSpec::System,
-            ).unwrap();
-            self.pointer = Some(themed_pointer);
-        }
-        if capability == Capability::Keyboard && self.keyboard.is_none() {
-            let keyboard = self.seat_state.get_keyboard(qh, &seat, None).unwrap();
-            self.keyboard = Some(keyboard);
-        }
-    }
-
-    fn remove_capability(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _seat: wl_seat::WlSeat,
-        capability: Capability,
-    ) {
-        if capability == Capability::Pointer {
-            self.pointer = None;
-        }
-        if capability == Capability::Keyboard {
-            self.keyboard = None;
-        }
-    }
-
-    fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
-        self.seats.retain(|s| s != &seat);
-    }
-}
-
-impl ShmHandler for App {
-    fn shm_state(&mut self) -> &mut Shm {
-        &mut self.shm_state
-    }
-}
-
-impl PointerHandler for App {
-    fn pointer_frame(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _pointer: &wl_pointer::WlPointer,
-        events: &[smithay_client_toolkit::seat::pointer::PointerEvent],
-    ) {
-        use smithay_client_toolkit::seat::pointer::PointerEventKind;
-        for event in events {
-            if let Some(st) = &mut self.state {
-                let (cx, cy) = clear_ui::wayland::scale_pointer_pos(event.position, st.scale_factor);
-                st.cursor_x = cx;
-                st.cursor_y = cy;
-            }
-            match &event.kind {
-                PointerEventKind::Motion { .. } => {
-                    if let Some(st) = &mut self.state {
-                        let cx = st.cursor_x;
-                        let cy = st.cursor_y;
-                        if st.handle_cursor_moved(cx, cy) {
-                            self.redraw = true;
-                        }
-                    }
-                }
-                PointerEventKind::Press { button, .. } => {
-                    let custom_btn = match button {
-                        272 => clear_ui::widget::MouseButton::Left,
-                        273 => clear_ui::widget::MouseButton::Right,
-                        _ => continue,
-                    };
-                    if let Some(st) = &mut self.state {
-                        if st.handle_mouse_input(custom_btn, clear_ui::widget::ElementState::Pressed) {
-                            self.redraw = true;
-                        }
-                    }
-                }
-                PointerEventKind::Release { button, .. } => {
-                    let custom_btn = match button {
-                        272 => clear_ui::widget::MouseButton::Left,
-                        273 => clear_ui::widget::MouseButton::Right,
-                        _ => continue,
-                    };
-                    if let Some(st) = &mut self.state {
-                        if st.handle_mouse_input(custom_btn, clear_ui::widget::ElementState::Released) {
-                            self.redraw = true;
-                        }
-                    }
-                }
-                PointerEventKind::Axis { horizontal, vertical, .. } => {
-                    let h_scroll = horizontal.absolute as f32;
-                    let v_scroll = vertical.absolute as f32;
-                    let delta = clear_ui::widget::MouseScrollDelta::LineDelta(-h_scroll / 10.0, -v_scroll / 10.0);
-                    if let Some(st) = &mut self.state {
-                        if st.handle_mouse_wheel(&delta) {
-                            self.redraw = true;
-                        }
-                    }
-                }
-                PointerEventKind::Enter { .. } => {
-                    if let Some(ref themed_pointer) = self.pointer {
-                        let _ = themed_pointer.set_cursor(_conn, CursorIcon::Default);
-                    }
-                }
-                _ => {}
-            }
-        }
-    }
-}
-
-impl KeyboardHandler for App {
-    fn enter(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _surface: &wl_surface::WlSurface,
-        _serial: u32,
-        _raw_modifiers: &[u32],
-        _keysyms: &[xkeysym::Keysym],
-    ) {}
-
-    fn leave(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _surface: &wl_surface::WlSurface,
-        _serial: u32,
-    ) {}
-
-    fn press_key(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _serial: u32,
-        event: smithay_client_toolkit::seat::keyboard::KeyEvent,
-    ) {
-        self.handle_key(event, clear_ui::widget::ElementState::Pressed);
-    }
-
-    fn release_key(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _serial: u32,
-        event: smithay_client_toolkit::seat::keyboard::KeyEvent,
-    ) {
-        self.handle_key(event, clear_ui::widget::ElementState::Released);
-    }
-
-    fn update_modifiers(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _serial: u32,
-        modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
-        _layout: u32,
-    ) {
-        self.ctrl_pressed = modifiers.ctrl;
-        self.shift_pressed = modifiers.shift;
-    }
-}
-
-impl App {
-    fn handle_key(&mut self, event: smithay_client_toolkit::seat::keyboard::KeyEvent, state: clear_ui::widget::ElementState) {
-        use clear_ui::widget::{Key, KeyEvent, NamedKey};
-        let logical_key = match event.keysym {
-            xkeysym::Keysym::Escape => Key::Named(NamedKey::Escape),
-            xkeysym::Keysym::Return => Key::Named(NamedKey::Enter),
-            xkeysym::Keysym::BackSpace => Key::Named(NamedKey::Backspace),
-            xkeysym::Keysym::Down => Key::Named(NamedKey::ArrowDown),
-            xkeysym::Keysym::Up => Key::Named(NamedKey::ArrowUp),
-            xkeysym::Keysym::Left => Key::Named(NamedKey::ArrowLeft),
-            xkeysym::Keysym::Right => Key::Named(NamedKey::ArrowRight),
-            xkeysym::Keysym::Tab => Key::Named(NamedKey::Tab),
-            xkeysym::Keysym::Delete => Key::Named(NamedKey::Delete),
-            xkeysym::Keysym::space => Key::Named(NamedKey::Space),
-            xkeysym::Keysym::j | xkeysym::Keysym::J => Key::Character("j".to_string()),
-            xkeysym::Keysym::k | xkeysym::Keysym::K => Key::Character("k".to_string()),
-            xkeysym::Keysym::u | xkeysym::Keysym::U => Key::Character("u".to_string()),
-            xkeysym::Keysym::i | xkeysym::Keysym::I => Key::Character("i".to_string()),
-            xkeysym::Keysym::n | xkeysym::Keysym::N => Key::Character("n".to_string()),
-            xkeysym::Keysym::p | xkeysym::Keysym::P => Key::Character("p".to_string()),
-            _ => {
-                if let Some(ref text) = event.utf8 {
-                    Key::Character(text.clone())
-                } else {
-                    return;
-                }
-            }
-        };
-
-        let custom_event = KeyEvent {
-            state,
-            logical_key,
-            text: event.utf8.clone(),
-            repeat: false,
-            ctrl: self.ctrl_pressed,
-            shift: self.shift_pressed,
-        };
-
-        if state == clear_ui::widget::ElementState::Pressed {
-            if is_repeatable_key(&custom_event.logical_key) {
-                self.pressed_key = Some(PressedKey {
-                    logical_key: custom_event.logical_key.clone(),
-                    text: custom_event.text.clone(),
-                    first_pressed: std::time::Instant::now(),
-                    last_repeated: std::time::Instant::now(),
-                });
-            } else {
-                self.pressed_key = None;
-            }
-        } else if state == clear_ui::widget::ElementState::Released {
-            if let Some(ref pk) = self.pressed_key {
-                if pk.logical_key == custom_event.logical_key {
-                    self.pressed_key = None;
-                }
-            }
-        }
-
-        if let Some(st) = &mut self.state {
-            if st.handle_key_input(&custom_event) {
-                self.redraw = true;
-            }
-        }
-    }
-}
-
-impl WindowHandler for App {
-    fn configure(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _window: &XdgWindow,
-        configure: WindowConfigure,
-        _serial: u32,
-    ) {
-        let (w, h) = configure.new_size;
-        if let (Some(w), Some(h)) = (w, h) {
-            let width = w.get();
-            let height = h.get();
-            if let Some(state) = &mut self.state {
-                let pw = (width as f64 * state.scale_factor) as u32;
-                let ph = (height as f64 * state.scale_factor) as u32;
-                state.resize(pw, ph);
-            }
-        }
-        self.redraw = true;
-    }
-
-    fn request_close(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _window: &XdgWindow) {
-        self.exit = true;
-    }
-}
-
-impl ProvidesRegistryState for App {
-    fn registry(&mut self) -> &mut RegistryState {
-        &mut self.registry_state
-    }
-    
-    fn runtime_add_global(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _name: u32,
-        _interface: &str,
-        _version: u32,
-    ) {}
-    
-    fn runtime_remove_global(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _name: u32,
-        _interface: &str,
-    ) {}
-}
-
-delegate_compositor!(App);
-delegate_xdg_shell!(App);
-delegate_xdg_window!(App);
-delegate_shm!(App);
-delegate_seat!(App);
-delegate_pointer!(App);
-delegate_keyboard!(App);
-delegate_registry!(App);
-delegate_output!(App);
-
 fn main() {
     let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
     let _guard = rt.enter();
@@ -3031,116 +2508,8 @@ fn main() {
         }
     }
 
-    let conn = Connection::connect_to_env().unwrap();
-    let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
-    let qh = event_queue.handle();
-
-    let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
-    let xdg_shell_state = XdgShell::bind(&globals, &qh).unwrap();
-    let shm_state = Shm::bind(&globals, &qh).unwrap();
-    let seat_state = SeatState::new(&globals, &qh);
-    let output_state = OutputState::new(&globals, &qh);
-
-    let mut app = App {
-        registry_state: RegistryState::new(&globals),
-        compositor_state,
-        xdg_shell_state,
-        shm_state,
-        seat_state,
-        output_state,
-        seats: Vec::new(),
-        pointer: None,
-        keyboard: None,
-        window: None,
-        surface: None,
-        state: None,
-        exit: false,
-        redraw: true,
-        ctrl_pressed: false,
-        shift_pressed: false,
-        pressed_key: None,
-    };
-
-    // Perform a roundtrip to populate output_state with active output scales
-    event_queue.roundtrip(&mut app).unwrap();
-
-    let scale = clear_ui::wayland::detect_scale_factor(&app.output_state);
-
-    let pw = (820.0 * scale) as u32;
-    let ph = (680.0 * scale) as u32;
-
-    let mut state = pollster::block_on(SystemInterface::new(
-        &conn,
-        &qh,
-        &app.compositor_state,
-        &app.xdg_shell_state,
-        pw,
-        ph,
-        scale,
-    ));
-    state.app.current_page = initial_page;
-    app.window = Some(state.window.clone());
-    app.surface = Some(state.surface.clone());
-    app.state = Some(state);
-
-    let mut event_loop = EventLoop::try_new().unwrap();
-    let loop_handle = event_loop.handle();
-    WaylandSource::new(conn, event_queue).insert(loop_handle.clone()).unwrap();
-
-    const KEY_REPEAT_DELAY: std::time::Duration = std::time::Duration::from_millis(500);
-    const KEY_REPEAT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
-
-    let mut last_tick = std::time::Instant::now();
-    loop {
-        event_loop
-            .dispatch(std::time::Duration::from_millis(16), &mut app)
-            .unwrap();
-        if app.exit {
-            break;
-        }
-
-        let now = std::time::Instant::now();
-        let mut dt = now.duration_since(last_tick).as_secs_f32();
-        last_tick = now;
-        if dt > 0.1 {
-            dt = 0.1;
-        }
-
-        if let Some(state) = &mut app.state {
-            state.tick(dt);
-            state.poll_background_updates();
-            if state.needs_rebuild {
-                app.redraw = true;
-            }
-        }
-
-        if let Some(ref mut pk) = app.pressed_key {
-            let now = std::time::Instant::now();
-            if now.duration_since(pk.first_pressed) >= KEY_REPEAT_DELAY {
-                if now.duration_since(pk.last_repeated) >= KEY_REPEAT_INTERVAL {
-                    pk.last_repeated = now;
-                    let custom_event = clear_ui::widget::KeyEvent {
-                        state: clear_ui::widget::ElementState::Pressed,
-                        logical_key: pk.logical_key.clone(),
-                        text: pk.text.clone(),
-                        repeat: true,
-                        ctrl: app.ctrl_pressed,
-                        shift: app.shift_pressed,
-                    };
-                    if let Some(st) = &mut app.state {
-                        if st.handle_key_input(&custom_event) {
-                            app.redraw = true;
-                        }
-                    }
-                }
-            }
-        }
+    let initial_page_idx = Page::ALL.iter().position(|&p| p == initial_page).unwrap_or(0);
+    INITIAL_PAGE_INDEX.store(initial_page_idx, std::sync::atomic::Ordering::SeqCst);
 
-        if app.redraw {
-            app.redraw = false;
-            if let Some(state) = &mut app.state {
-                state.render();
-            }
-        }
-    }
+    clear_ui::engine::run::<SystemInterface>();
 }
diff --git a/src/pages/colors.rs b/src/pages/colors.rs
index 6659c9b..169547b 100644
--- a/src/pages/colors.rs
+++ b/src/pages/colors.rs
@@ -309,7 +309,7 @@ pub fn view(state: &mut ColorsState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Pag
     state.color_selectors[6].color = state.color_borders_color;
     sec.widget(&mut pc, &mut state.color_selectors[6], 12.0, 220.0, 22.0);
     sec.spacing(8.0);
-    y = sec.finish(&mut pc);
+    sec.finish(&mut pc);
 
     pc
 }
diff --git a/src/pages/layout.rs b/src/pages/layout.rs
index 0ea1459..e2ab962 100644
--- a/src/pages/layout.rs
+++ b/src/pages/layout.rs
@@ -3,7 +3,7 @@ use std::io::Write;
 
 use crate::app::PageContent;
 use clear_ui::layout::Section;
-use clear_ui::widget::Spinbox;
+use clear_ui::widget::{Dropdown, Spinbox};
 
 const CONFIG_PATH: &str = "/home/lsgalante/.config/ccec/config.toml";
 
@@ -66,6 +66,7 @@ pub struct LayoutState {
     pub edge_gap_spinbox: Spinbox,
     pub top_gap_spinbox: Spinbox,
     pub transition_duration_spinbox: Spinbox,
+    pub tag_layout_menus: Vec<Dropdown>,
 }
 
 impl Default for LayoutState {
@@ -84,6 +85,18 @@ impl Default for LayoutState {
             edge_gap_spinbox: Spinbox::new(48, 0, 200, 1),
             top_gap_spinbox: Spinbox::new(48, 0, 200, 1),
             transition_duration_spinbox: Spinbox::new(300, 0, 2000, 50),
+            tag_layout_menus: (1..=4).map(|i| {
+                Dropdown::new(
+                    vec![
+                        "Cascade".to_string(),
+                        "Grid".to_string(),
+                        "Fullscreen".to_string(),
+                        "Floating".to_string(),
+                        "Popup".to_string(),
+                    ],
+                    0
+                ).with_label(&format!("Tag {}", i))
+            }).collect(),
         }
     }
 }
@@ -95,6 +108,7 @@ pub enum LayoutMessage {
     SetEdgeGap(u16),
     SetTopGap(u16),
     SetTransitionDuration(u16),
+    SetTagLayout(usize, usize),
     Refreshed(LayoutState),
 }
 
@@ -108,6 +122,21 @@ pub fn read_layout_config() -> LayoutState {
     let gl = parse_u16_from(&content, "gap_left", 48);
     let gt = parse_u16_from(&content, "gap_top", 48);
     let td = parse_u16_from(&content, "transition_duration", 300);
+
+    let tag_modes = parse_tag_layouts_from_config(&content);
+    let dropdown_options = vec![
+        "Cascade".to_string(),
+        "Grid".to_string(),
+        "Fullscreen".to_string(),
+        "Floating".to_string(),
+        "Popup".to_string(),
+    ];
+    let tag_layout_menus = (1..=4).map(|i| {
+        let mode_str = &tag_modes[i - 1];
+        let idx = dropdown_options.iter().position(|opt| opt.to_lowercase() == mode_str.to_lowercase()).unwrap_or(0);
+        Dropdown::new(dropdown_options.clone(), idx).with_label(&format!("Tag {}", i))
+    }).collect();
+
     LayoutState {
         fullscreen_border_width: fs,
         cascade_border_width: ca,
@@ -122,7 +151,101 @@ pub fn read_layout_config() -> LayoutState {
         edge_gap_spinbox: Spinbox::new(gl as i32, 0, 200, 1),
         top_gap_spinbox: Spinbox::new(gt as i32, 0, 200, 1),
         transition_duration_spinbox: Spinbox::new(td as i32, 0, 2000, 50),
+        tag_layout_menus,
+    }
+}
+
+fn parse_tag_layouts_from_config(content: &str) -> Vec<String> {
+    let mut modes = vec!["cascade".to_string(); 4];
+    let mut current_tag = None;
+    let mut current_mode = None;
+    
+    for line in content.lines() {
+        let trimmed = line.trim();
+        if trimmed == "[[tag_layout]]" {
+            if let (Some(tag), Some(mode)) = (current_tag, current_mode.take()) {
+                if tag >= 1 && tag <= 4 {
+                    modes[tag - 1] = mode;
+                }
+            }
+            current_tag = None;
+            continue;
+        }
+        if trimmed.starts_with('[') && !trimmed.starts_with("[[") {
+            if let (Some(tag), Some(mode)) = (current_tag, current_mode.take()) {
+                if tag >= 1 && tag <= 4 {
+                    modes[tag - 1] = mode;
+                }
+            }
+            current_tag = None;
+            continue;
+        }
+        if let Some(rest) = trimmed.strip_prefix("tag") {
+            let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=').trim();
+            if let Ok(tag) = rest.parse::<usize>() {
+                current_tag = Some(tag);
+            }
+        } else if let Some(rest) = trimmed.strip_prefix("mode") {
+            let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=').trim().trim_matches('"').to_string();
+            current_mode = Some(rest);
+        }
     }
+    if let (Some(tag), Some(mode)) = (current_tag, current_mode.take()) {
+        if tag >= 1 && tag <= 4 {
+            modes[tag - 1] = mode;
+        }
+    }
+    modes
+}
+
+fn write_tag_layout(tag_num: usize, mode_str: &str) -> bool {
+    let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+    let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
+    
+    let mut i = 0;
+    while i < lines.len() {
+        let line = lines[i].trim();
+        if line == "[[tag_layout]]" {
+            let mut tag_val = None;
+            let mut mode_line_idx = None;
+            
+            let mut j = i + 1;
+            while j < lines.len() {
+                let next_line = lines[j].trim();
+                if next_line.starts_with("[[") || (next_line.starts_with('[') && !next_line.starts_with("[[")) {
+                    break;
+                }
+                if next_line.starts_with("tag") {
+                    if let Some(val_str) = next_line.split('=').nth(1) {
+                        if let Ok(v) = val_str.trim().parse::<usize>() {
+                            tag_val = Some(v);
+                        }
+                    }
+                } else if next_line.starts_with("mode") {
+                    mode_line_idx = Some(j);
+                }
+                j += 1;
+            }
+            
+            if tag_val == Some(tag_num) {
+                if let Some(idx) = mode_line_idx {
+                    lines[idx] = format!("mode = \"{}\"", mode_str.to_lowercase());
+                    let result = lines.join("\n") + "\n";
+                    return fs::write(CONFIG_PATH, result).is_ok();
+                }
+            }
+            i = j;
+        } else {
+            i += 1;
+        }
+    }
+    
+    let mut result = lines.join("\n");
+    if !result.ends_with('\n') {
+        result.push('\n');
+    }
+    result.push_str(&format!("\n[[tag_layout]]\ntag = {}\nmode = \"{}\"\n", tag_num, mode_str.to_lowercase()));
+    fs::write(CONFIG_PATH, result).is_ok()
 }
 
 fn parse_u16_from(content: &str, key: &str, default: u16) -> u16 {
@@ -218,6 +341,19 @@ pub fn view(state: &mut LayoutState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_fo
     movement_sec.spacing(8.0);
     y = movement_sec.finish_focused(&mut pc, sec_focused.get(2).copied().unwrap_or(false));
 
+    // 4. Default Layouts Section
+    let mut default_layouts_sec = Section::new(&mut pc, cx, y, cw, "Default Layouts");
+    default_layouts_sec.spacing(8.0);
+    for i in 0..4 {
+        default_layouts_sec.widget(&mut pc, &mut state.tag_layout_menus[i], 14.0, 200.0, 26.0);
+        default_layouts_sec.spacing(8.0);
+    }
+    default_layouts_sec.finish_focused(&mut pc, sec_focused.get(3).copied().unwrap_or(false));
+
+    for menu in &mut state.tag_layout_menus {
+        menu.render_popover(&mut pc);
+    }
+
     pc
 }
 
@@ -269,6 +405,71 @@ pub fn update(state: &mut LayoutState, msg: LayoutMessage) {
             state.transition_duration_spinbox.value = val as i32;
             apply_all_widths(state);
         }
+        LayoutMessage::SetTagLayout(tag, idx) => {
+            if tag >= 1 && tag <= 4 && idx < 5 {
+                state.tag_layout_menus[tag - 1].selected = idx;
+                let modes = vec!["cascade", "grid", "fullscreen", "floating", "popup"];
+                let mode_str = modes[idx];
+                write_tag_layout(tag, mode_str);
+                send_ipc_command(&format!("tag-layout {} {}", tag, mode_str));
+            }
+        }
         LayoutMessage::Refreshed(new) => { *state = new; }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_parse_tag_layouts_empty() {
+        let content = "";
+        let modes = parse_tag_layouts_from_config(content);
+        assert_eq!(modes, vec!["cascade", "cascade", "cascade", "cascade"]);
+    }
+
+    #[test]
+    fn test_parse_tag_layouts_single() {
+        let content = r#"
+[layout]
+gap = 10
+
+[[tag_layout]]
+tag = 2
+mode = "grid"
+"#;
+        let modes = parse_tag_layouts_from_config(content);
+        assert_eq!(modes, vec!["cascade", "grid", "cascade", "cascade"]);
+    }
+
+    #[test]
+    fn test_parse_tag_layouts_multiple() {
+        let content = r#"
+[[tag_layout]]
+tag = 1
+mode = "fullscreen"
+
+[[tag_layout]]
+tag = 4
+mode = "floating"
+"#;
+        let modes = parse_tag_layouts_from_config(content);
+        assert_eq!(modes, vec!["fullscreen", "cascade", "cascade", "floating"]);
+    }
+
+    #[test]
+    fn test_parse_tag_layouts_out_of_bounds() {
+        let content = r#"
+[[tag_layout]]
+tag = 5
+mode = "grid"
+
+[[tag_layout]]
+tag = 0
+mode = "popup"
+"#;
+        let modes = parse_tag_layouts_from_config(content);
+        assert_eq!(modes, vec!["cascade", "cascade", "cascade", "cascade"]);
+    }
+}