Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
Implement per-output display scale configuration and menus support
Makefile | 2 +
scripts/cce-app-menu | 92 +++++++++++++++++++++++++++++++++++++++++
scripts/cce-desktop-menu | 75 +++++++++++++++++++++++++++++++++
src/server/config.rs | 16 ++++++++
src/server/cursor.rs | 54 +++++++++++++++++++++++-
src/server/output.rs | 98 ++++++++++++++++++++++++--------------------
src/server/output_manager.rs | 5 +++
src/server/window_manager.rs | 23 +++++++++++
8 files changed, 319 insertions(+), 46 deletions(-)
diff --git a/Makefile b/Makefile
index fbba089..0f59205 100644
--- a/Makefile
+++ b/Makefile
@@ -10,6 +10,8 @@ install: build
else \
echo "Error: cce binary not found"; exit 1; \
fi
+ install -m 755 scripts/cce-desktop-menu ~/.local/bin/cce-desktop-menu
+ install -m 755 scripts/cce-app-menu ~/.local/bin/cce-app-menu
run:
cargo run --bin cce
diff --git a/scripts/cce-app-menu b/scripts/cce-app-menu
new file mode 100644
index 0000000..3112702
--- /dev/null
+++ b/scripts/cce-app-menu
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+# cce-app-menu — Context menu for CCE application windows
+
+CLEARCTL="/home/lsgalante/.local/bin/cce control"
+CLEAR_CLOUD="/home/lsgalante/.local/bin/clear-cloud"
+
+# Default to empty/none
+x_arg=""
+y_arg=""
+window_index=""
+app_id=""
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -x|--x-pos)
+ x_arg="-x $2"
+ shift 2
+ ;;
+ -y|--y-pos)
+ y_arg="-y $2"
+ shift 2
+ ;;
+ -i|--window-id)
+ window_index="$2"
+ shift 2
+ ;;
+ -a|--app-id)
+ app_id="$2"
+ shift 2
+ ;;
+ *)
+ shift
+ ;;
+ esac
+done
+
+if [[ -z "$window_index" || -z "$app_id" ]]; then
+ echo "Usage: cce-app-menu -i <window_index> -a <app_id> [-x <x>] [-y <y>]" >&2
+ exit 1
+fi
+
+# Define the JSON layout for the application context menu
+json_layout='{
+ "pages": [
+ {
+ "title": "'"${app_id}"' Window Options",
+ "justify": "left",
+ "widgets": [
+ { "type": "button", "text": "Maximize", "id": "maximize" },
+ { "type": "button", "text": "Tile (Grid)", "id": "tile_grid" },
+ { "type": "button", "text": "Float", "id": "float" },
+ { "type": "button", "text": "Fullscreen", "id": "fullscreen" },
+ { "type": "button", "text": "Minimize", "id": "minimize" },
+ { "type": "button", "text": "Close Window", "id": "close" }
+ ]
+ }
+ ]
+}'
+
+# Spawn clear-cloud with the json layout and coordinates
+selected=$(echo "$json_layout" | $CLEAR_CLOUD --json $x_arg $y_arg 2>/dev/null)
+
+[[ -z "$selected" ]] && exit 0
+
+# Parse button using python for reliability and robustness
+btn=$(echo "$selected" | python3 -c "import sys, json; print(json.load(sys.stdin).get('button', ''))")
+
+[[ -z "$btn" ]] && exit 0
+
+# First focus the window so the commands apply to it
+$CLEARCTL focus-window "$window_index"
+
+case "$btn" in
+ "maximize")
+ $CLEARCTL mode maximized "$app_id"
+ ;;
+ "tile_grid")
+ $CLEARCTL mode grid "$app_id"
+ ;;
+ "float")
+ $CLEARCTL mode floating "$app_id"
+ ;;
+ "fullscreen")
+ $CLEARCTL mode fullscreen "$app_id"
+ ;;
+ "minimize")
+ $CLEARCTL minimize
+ ;;
+ "close")
+ $CLEARCTL close
+ ;;
+esac
diff --git a/scripts/cce-desktop-menu b/scripts/cce-desktop-menu
new file mode 100644
index 0000000..e7732cf
--- /dev/null
+++ b/scripts/cce-desktop-menu
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+# cce-desktop-menu — Context menu for CCE desktop background
+
+CLEARCTL="/home/lsgalante/.local/bin/cce control"
+CLEAR_CLOUD="/home/lsgalante/.local/bin/clear-cloud"
+
+# Default to empty coordinates if not provided
+x_arg=""
+y_arg=""
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -x|--x-pos)
+ x_arg="-x $2"
+ shift 2
+ ;;
+ -y|--y-pos)
+ y_arg="-y $2"
+ shift 2
+ ;;
+ *)
+ shift
+ ;;
+ esac
+done
+
+# Define the JSON layout for the desktop context menu
+json_layout='{
+ "pages": [
+ {
+ "title": "Desktop Context Menu",
+ "justify": "left",
+ "widgets": [
+ { "type": "button", "text": "Terminal", "id": "terminal" },
+ { "type": "button", "text": "Applications", "id": "apps" },
+ { "type": "button", "text": "System Settings", "id": "settings" },
+ { "type": "button", "text": "Expose Windows", "id": "expose" },
+ { "type": "button", "text": "Reload Config", "id": "reload" },
+ { "type": "button", "text": "Exit CCE", "id": "exit" }
+ ]
+ }
+ ]
+}'
+
+# Spawn clear-cloud with the json layout and coordinates
+selected=$(echo "$json_layout" | $CLEAR_CLOUD --json $x_arg $y_arg 2>/dev/null)
+
+[[ -z "$selected" ]] && exit 0
+
+# Parse button using python for reliability and robustness
+btn=$(echo "$selected" | python3 -c "import sys, json; print(json.load(sys.stdin).get('button', ''))")
+
+case "$btn" in
+ "terminal")
+ # Run terminal
+ foot &
+ ;;
+ "apps")
+ # Run applications search/list
+ $CLEAR_CLOUD --mode apps &
+ ;;
+ "settings")
+ # Run settings interface
+ /home/lsgalante/.local/bin/cce-system-settings &
+ ;;
+ "expose")
+ $CLEARCTL expose
+ ;;
+ "reload")
+ $CLEARCTL reload
+ ;;
+ "exit")
+ $CLEARCTL exit
+ ;;
+esac
diff --git a/src/server/config.rs b/src/server/config.rs
index 082947a..c45ad6d 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -289,6 +289,8 @@ pub struct Config {
#[serde(default)]
pub output: Option<OutputConfig>,
#[serde(default)]
+ pub display: HashMap<String, f64>,
+ #[serde(default)]
pub device: Vec<InputDeviceConfigRule>,
#[serde(default)]
pub input: Option<InputConfig>,
@@ -712,6 +714,7 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
};
state.output_scale = config.output.as_ref().map(|o| o.scale as f32).unwrap_or(1.0f32);
+ state.display = config.display.clone();
state.layout.gap = config.layout.gap as i32;
state.layout.gap_top = config.layout.gap_top as i32;
@@ -989,4 +992,17 @@ mod tests {
assert_eq!(server.wm.layout.background_b, b * 0x01010101);
}
}
+
+ #[test]
+ fn test_display_scale_parsing() {
+ let content = r#"{
+ "display": {
+ "scale_eDP-1": 2.0,
+ "scale_DP-1": 1.5
+ }
+ }"#;
+ let config: Config = serde_json::from_str(content).unwrap();
+ assert_eq!(config.display.get("scale_eDP-1"), Some(&2.0));
+ assert_eq!(config.display.get("scale_DP-1"), Some(&1.5));
+ }
}
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index d846d32..abb3549 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -49,6 +49,8 @@ pub struct Cursor {
pub panning_gesture_active: bool,
pub last_click_time: u32,
pub last_click_window: *mut crate::window::Window,
+ pub right_click_on_bg: bool,
+ pub right_click_on_border: bool,
}
impl Default for Cursor {
@@ -95,6 +97,8 @@ impl Default for Cursor {
panning_gesture_active: false,
last_click_time: 0,
last_click_window: std::ptr::null_mut(),
+ right_click_on_bg: false,
+ right_click_on_border: false,
}
}
}
@@ -663,6 +667,30 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
} else {
0
};
+
+ if (*event).button == 0x111 && modifiers == 0 {
+ let mut clicked_interactive = false;
+ if let Some(result) = (*server).scene.at(lx, ly) {
+ match result.data {
+ SceneNodeDataVal::Window(_) | SceneNodeDataVal::LayerSurface(_) | SceneNodeDataVal::ShellSurface(_) | SceneNodeDataVal::LockSurface(_) | SceneNodeDataVal::OverrideRedirect(_) => {
+ clicked_interactive = true;
+ }
+ }
+ }
+ if !clicked_interactive {
+ cursor.right_click_on_bg = true;
+ let x = cursor.x() as i32;
+ let y = cursor.y() as i32;
+ let cmd = format!("/home/lsgalante/.local/bin/cce-desktop-menu -x {} -y {}", x, y);
+ (*server).wm.execute_action(&crate::config::Action::Spawn, Some(&cmd));
+
+ seat.focus(Focus::None);
+ (*(*seat).server).wm.dirty_windowing();
+
+ cursor.pressed.insert((*event).button, None);
+ return;
+ }
+ }
let mut matched_pb: Option<crate::config::PointerBind> = None;
for pb in &(*(*seat).server).wm.pointer_binds {
@@ -774,7 +802,21 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
&& (*border_target_win).tiling_mode != crate::tiling::TilingMode::Fullscreen
) {
let initial_mode = (*border_target_win).tiling_mode;
- match get_border_zone(border_target_win, lx, ly) {
+ let zone = get_border_zone(border_target_win, lx, ly);
+ if (*event).button == 0x111 && modifiers == 0 && !matches!(zone, BorderZone::None) {
+ cursor.right_click_on_border = true;
+ let x = cursor.x() as i32;
+ let y = cursor.y() as i32;
+ let index = (*border_target_win).ref_key.index;
+ let app_id = (*border_target_win).get_app_id_string().unwrap_or_else(|| "unknown".to_string());
+ let cmd = format!("/home/lsgalante/.local/bin/cce-app-menu -x {} -y {} -i {} -a {}", x, y, index, app_id);
+ (*server).wm.execute_action(&crate::config::Action::Spawn, Some(&cmd));
+
+ cursor.pressed.insert((*event).button, None);
+ return;
+ }
+
+ match zone {
BorderZone::Resize(edges) => {
if (*event).button == 0x110 { // BTN_LEFT
if initial_mode != crate::tiling::TilingMode::Floating {
@@ -1032,6 +1074,16 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
return;
}
+ if (*event).button == 0x111 && (cursor.right_click_on_bg || cursor.right_click_on_border) {
+ cursor.right_click_on_bg = false;
+ cursor.right_click_on_border = false;
+ if cursor.pressed.is_empty() && seat.op.is_some() {
+ seat.op_release = true;
+ (*(*seat).server).wm.dirty_windowing();
+ }
+ return;
+ }
+
if !should_block_button {
ffi::wlr_seat_pointer_notify_button(
seat.wlr_seat,
diff --git a/src/server/output.rs b/src/server/output.rs
index a3a6e75..a08ae7f 100644
--- a/src/server/output.rs
+++ b/src/server/output.rs
@@ -364,12 +364,19 @@ impl Output {
return Err("Failed to create wlr_scene_output");
}
+ let name_raw = ffi::river_wlr_output_get_name(wlr_output);
+ let name = std::ffi::CStr::from_ptr(name_raw).to_string_lossy();
+ let scale_key = format!("scale_{}", name);
+ let output_scale = (*server).wm.display.get(&scale_key)
+ .map(|&s| s as f32)
+ .unwrap_or((*server).wm.output_scale);
+
let initial = OutputState {
state: OutputStateValue::DisabledHard,
x: 0,
y: 0,
mode: OutputMode::None,
- scale: (*server).wm.output_scale,
+ scale: output_scale,
transform: ffi::wl_output_transform_WL_OUTPUT_TRANSFORM_NORMAL,
adaptive_sync: ffi::river_wlr_output_get_adaptive_sync_status(wlr_output) == ffi::wlr_output_adaptive_sync_status_WLR_OUTPUT_ADAPTIVE_SYNC_ENABLED,
auto_layout: true,
@@ -535,13 +542,17 @@ impl Output {
let (viewport_w, viewport_h) = self.current.dimensions();
let zoom = wm.desk_zoom;
- // Dynamic spacing based on zoom to avoid rendering too many lines (LOD)
- let mut grid_spacing = wm.layout.desktop_grid_scale;
- while grid_spacing * zoom < 40.0 {
- grid_spacing *= 2.0;
+ // LOD calculations for cells and gaps
+ let mut cell_size = wm.layout.desktop_grid_scale.max(5.0);
+ let mut gap_size = (wm.layout.desktop_line_width as f64).max(0.0);
+ let mut period = cell_size + gap_size;
+
+ while period * zoom < 40.0 {
+ cell_size *= 2.0;
+ gap_size *= 2.0;
+ period = cell_size + gap_size;
}
- let line_width = wm.layout.desktop_line_width;
let grid_color: [f32; 4] = wm.layout.desktop_grid_color;
// Check if cached grid parameters match current parameters
@@ -550,8 +561,8 @@ impl Output {
&& self.last_grid_zoom == zoom
&& self.last_grid_pan_x == wm.desk_pan_x
&& self.last_grid_pan_y == wm.desk_pan_y
- && self.last_grid_spacing == grid_spacing
- && self.last_grid_line_width == line_width
+ && self.last_grid_spacing == cell_size
+ && self.last_grid_line_width == gap_size as i32
&& self.last_grid_color == grid_color
{
return;
@@ -563,11 +574,11 @@ impl Output {
self.last_grid_zoom = zoom;
self.last_grid_pan_x = wm.desk_pan_x;
self.last_grid_pan_y = wm.desk_pan_y;
- self.last_grid_spacing = grid_spacing;
- self.last_grid_line_width = line_width;
+ self.last_grid_spacing = cell_size;
+ self.last_grid_line_width = gap_size as i32;
self.last_grid_color = grid_color;
- // Clear previous grid lines
+ // Clear previous grid rendering
ffi::river_scene_tree_clear_children(self.grid_tree);
let min_x = wm.desk_pan_x;
@@ -575,44 +586,41 @@ impl Output {
let min_y = wm.desk_pan_y;
let max_y = wm.desk_pan_y + (viewport_h as f64) / zoom;
- // Draw vertical lines
- let mut x_val = (min_x / grid_spacing).ceil() * grid_spacing;
- while x_val <= max_x {
- let rel_x = ((x_val - wm.desk_pan_x) * zoom) as i32;
- let line_rect = ffi::wlr_scene_rect_create(
- self.grid_tree,
- line_width, // width of line
- viewport_h,
- grid_color.as_ptr(),
- );
- if !line_rect.is_null() {
- ffi::wlr_scene_node_set_position(
- line_rect as *mut ffi::wlr_scene_node,
- rel_x,
- 0,
- );
+ let min_col = ((min_x - cell_size) / period).floor() as i32;
+ let max_col = (max_x / period).ceil() as i32;
+ let min_row = ((min_y - cell_size) / period).floor() as i32;
+ let max_row = (max_y / period).ceil() as i32;
+
+ for col in min_col..=max_col {
+ let vx = (col as f64) * period;
+ let rel_x = ((vx - wm.desk_pan_x) * zoom) as i32;
+ let rw = (cell_size * zoom) as i32;
+ if rw <= 0 {
+ continue;
}
- x_val += grid_spacing;
- }
- // Draw horizontal lines
- let mut y_val = (min_y / grid_spacing).ceil() * grid_spacing;
- while y_val <= max_y {
- let rel_y = ((y_val - wm.desk_pan_y) * zoom) as i32;
- let line_rect = ffi::wlr_scene_rect_create(
- self.grid_tree,
- viewport_w,
- line_width, // height of line
- grid_color.as_ptr(),
- );
- if !line_rect.is_null() {
- ffi::wlr_scene_node_set_position(
- line_rect as *mut ffi::wlr_scene_node,
- 0,
- rel_y,
+ for row in min_row..=max_row {
+ let vy = (row as f64) * period;
+ let rel_y = ((vy - wm.desk_pan_y) * zoom) as i32;
+ let rh = (cell_size * zoom) as i32;
+ if rh <= 0 {
+ continue;
+ }
+
+ let cell_rect = ffi::wlr_scene_rect_create(
+ self.grid_tree,
+ rw,
+ rh,
+ grid_color.as_ptr(),
);
+ if !cell_rect.is_null() {
+ ffi::wlr_scene_node_set_position(
+ cell_rect as *mut ffi::wlr_scene_node,
+ rel_x,
+ rel_y,
+ );
+ }
}
- y_val += grid_spacing;
}
}
}
diff --git a/src/server/output_manager.rs b/src/server/output_manager.rs
index dd94da0..4a1bd13 100644
--- a/src/server/output_manager.rs
+++ b/src/server/output_manager.rs
@@ -305,6 +305,11 @@ impl OutputManager {
break;
}
+ if output.sent.scale != output.current.scale {
+ need_modeset = true;
+ break;
+ }
+
let wlr_adaptive = ffi::river_wlr_output_get_adaptive_sync_status(wlr_output)
== ffi::wlr_output_adaptive_sync_status_WLR_OUTPUT_ADAPTIVE_SYNC_ENABLED;
if output.sent.adaptive_sync != wlr_adaptive {
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 20a8faf..7ac9260 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -102,6 +102,7 @@ pub struct WindowManager {
pub startup_pids: Vec<(crate::config::StartupConfig, nix::unistd::Pid)>,
pub status_sender: Option<crate::status_server::StatusSender>,
pub output_scale: f32,
+ pub display: std::collections::HashMap<String, f64>,
pub input_rules: Vec<crate::config::InputDeviceConfigRule>,
pub input_config: crate::config::InputConfig,
pub last_status_update: std::cell::RefCell<Option<crate::status_server::StatusUpdate>>,
@@ -122,6 +123,7 @@ impl WindowManager {
self.scheduled.output_config = std::ptr::null_mut();
self.sent.output_config = std::ptr::null_mut();
self.output_scale = 1.0;
+ self.display = std::collections::HashMap::new();
self.input_rules = Vec::new();
self.input_config = crate::config::InputConfig::default();
self.mode = WindowManagerMode::Normal;
@@ -166,6 +168,7 @@ impl WindowManager {
self.shutting_down = false;
self.layout = crate::config::Layout::default();
self.output_scale = 1.0;
+ self.display = std::collections::HashMap::new();
self.has_restored_focused_window = false;
self.restored_focused_window_mapped = false;
self.mode_rules = Vec::new();
@@ -2487,6 +2490,26 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
let old_pids = std::mem::take(&mut self.startup_pids);
match crate::config::parse_config(&path, self) {
Ok(()) => {
+ // Update scales of existing outputs from the newly loaded config
+ let om_outputs = &mut (*self.server).om.outputs as *mut ffi::wl_list;
+ let mut link = (*om_outputs).next;
+ while link != om_outputs {
+ let output = &mut *crate::container_of!(link, crate::output::Output, link);
+ let wlr_output = output.wlr_output;
+ if !wlr_output.is_null() {
+ let name_raw = ffi::river_wlr_output_get_name(wlr_output);
+ let name = std::ffi::CStr::from_ptr(name_raw).to_string_lossy();
+ let scale_key = format!("scale_{}", name);
+ let output_scale = self.display.get(&scale_key)
+ .map(|&s| s as f32)
+ .unwrap_or(self.output_scale);
+ if output.scheduled.scale != output_scale {
+ output.scheduled.scale = output_scale;
+ }
+ }
+ link = (*link).next;
+ }
+
self.dirty_windowing();
// Process old PIDs