@@ -578,7 +578,7 @@ impl Daemon { |
| 578 | 578 | /// Handle an IPC command |
| 579 | 579 | fn handle_command(&mut self, cmd: Command) -> Response { |
| 580 | 580 | match cmd { |
| 581 | | - Command::Set { source, mode, monitor: _, interval_secs, shuffle, animate, max_fps } => { |
| 581 | + Command::Set { source, mode, monitor: _, interval_secs, shuffle, animate, max_fps, span } => { |
| 582 | 582 | let scale_mode = mode.unwrap_or(self.state.config.general.mode); |
| 583 | 583 | |
| 584 | 584 | // Stop any existing animation first |
@@ -629,7 +629,7 @@ impl Daemon { |
| 629 | 629 | } |
| 630 | 630 | |
| 631 | 631 | // Set up slideshow with the new source (static image) |
| 632 | | - match self.set_wallpaper_with_options(&source, scale_mode, shuffle, interval_secs) { |
| 632 | + match self.set_wallpaper_with_options(&source, scale_mode, shuffle, interval_secs, span) { |
| 633 | 633 | Ok(_) => { |
| 634 | 634 | // Update slideshow interval |
| 635 | 635 | self.state.slideshow_interval = interval_secs.map(Duration::from_secs); |
@@ -645,9 +645,9 @@ impl Daemon { |
| 645 | 645 | } |
| 646 | 646 | Command::SetWorkspace { workspace, source, mode } => { |
| 647 | 647 | let scale_mode = mode.unwrap_or(self.state.config.general.mode); |
| 648 | | - // Only set if we're on this workspace |
| 648 | + // Only set if we're on this workspace (default to per-monitor) |
| 649 | 649 | if self.state.current_workspace == workspace { |
| 650 | | - match self.set_wallpaper_from_source(&source, scale_mode, false) { |
| 650 | + match self.set_wallpaper_from_source(&source, scale_mode, false, false) { |
| 651 | 651 | Ok(_) => Response::ok(), |
| 652 | 652 | Err(e) => Response::error(e.to_string()), |
| 653 | 653 | } |
@@ -853,7 +853,8 @@ impl Daemon { |
| 853 | 853 | return Ok(()); |
| 854 | 854 | } |
| 855 | 855 | |
| 856 | | - self.set_wallpaper_from_source(&source, mode, shuffle) |
| 856 | + // Default to per-monitor (span = false) |
| 857 | + self.set_wallpaper_from_source(&source, mode, shuffle, false) |
| 857 | 858 | } |
| 858 | 859 | |
| 859 | 860 | /// Start an animated image playback (GIF, WebP, APNG, or video) |
@@ -1084,12 +1085,13 @@ impl Daemon { |
| 1084 | 1085 | mode: ScaleMode, |
| 1085 | 1086 | shuffle: bool, |
| 1086 | 1087 | _interval_secs: Option<u64>, |
| 1088 | + span: bool, |
| 1087 | 1089 | ) -> Result<()> { |
| 1088 | | - self.set_wallpaper_from_source(source, mode, shuffle) |
| 1090 | + self.set_wallpaper_from_source(source, mode, shuffle, span) |
| 1089 | 1091 | } |
| 1090 | 1092 | |
| 1091 | 1093 | /// Set wallpaper from a source (file, directory, or URL) |
| 1092 | | - fn set_wallpaper_from_source(&mut self, source: &str, mode: ScaleMode, shuffle: bool) -> Result<()> { |
| 1094 | + fn set_wallpaper_from_source(&mut self, source: &str, mode: ScaleMode, shuffle: bool, span: bool) -> Result<()> { |
| 1093 | 1095 | // Expand path |
| 1094 | 1096 | let expanded = shellexpand::tilde(source); |
| 1095 | 1097 | let path = std::path::Path::new(expanded.as_ref()); |
@@ -1118,7 +1120,7 @@ impl Daemon { |
| 1118 | 1120 | playlist.save()?; |
| 1119 | 1121 | self.state.playlist = Some(playlist); |
| 1120 | 1122 | |
| 1121 | | - self.set_wallpaper(&first, mode)?; |
| 1123 | + self.set_wallpaper_with_span(&first, mode, span)?; |
| 1122 | 1124 | |
| 1123 | 1125 | tracing::info!( |
| 1124 | 1126 | "Playlist loaded: {} images{}", |
@@ -1128,14 +1130,10 @@ impl Daemon { |
| 1128 | 1130 | } else if source.starts_with("http://") || source.starts_with("https://") { |
| 1129 | 1131 | // Remote URL |
| 1130 | 1132 | let image = self.fetch_image(source)?; |
| 1131 | | - let conn = self.conn_mut()?; |
| 1132 | | - let (width, height) = conn.screen_dimensions(); |
| 1133 | | - let scaled = scale_image(&image, width as u32, height as u32, mode); |
| 1134 | | - conn.set_wallpaper(&scaled)?; |
| 1135 | | - tracing::info!("Wallpaper set: {} (mode: {})", source, mode); |
| 1133 | + self.set_image_with_span(&image, source, mode, span)?; |
| 1136 | 1134 | } else { |
| 1137 | 1135 | // Single file |
| 1138 | | - self.set_wallpaper(source, mode)?; |
| 1136 | + self.set_wallpaper_with_span(source, mode, span)?; |
| 1139 | 1137 | } |
| 1140 | 1138 | |
| 1141 | 1139 | Ok(()) |
@@ -1143,14 +1141,43 @@ impl Daemon { |
| 1143 | 1141 | |
| 1144 | 1142 | /// Set wallpaper from a local file |
| 1145 | 1143 | pub fn set_wallpaper(&mut self, source: &str, mode: ScaleMode) -> Result<()> { |
| 1144 | + self.set_wallpaper_with_span(source, mode, false) |
| 1145 | + } |
| 1146 | + |
| 1147 | + /// Set wallpaper from a local file with span option |
| 1148 | + pub fn set_wallpaper_with_span(&mut self, source: &str, mode: ScaleMode, span: bool) -> Result<()> { |
| 1146 | 1149 | let expanded = shellexpand::tilde(source); |
| 1147 | 1150 | let image = ImageLoader::load_file(expanded.as_ref())?; |
| 1148 | | - let conn = self.conn_mut()?; |
| 1149 | | - let (width, height) = conn.screen_dimensions(); |
| 1150 | | - let scaled = scale_image(&image, width as u32, height as u32, mode); |
| 1151 | | - conn.set_wallpaper(&scaled)?; |
| 1151 | + self.set_image_with_span(&image, source, mode, span) |
| 1152 | + } |
| 1153 | + |
| 1154 | + /// Set wallpaper from an image with span option |
| 1155 | + fn set_image_with_span(&mut self, image: &image::RgbaImage, source: &str, mode: ScaleMode, span: bool) -> Result<()> { |
| 1156 | + let conn = self.conn()?; |
| 1157 | + let monitors = Monitor::get_all(conn).unwrap_or_default(); |
| 1152 | 1158 | |
| 1153 | | - tracing::info!("Wallpaper set: {} (mode: {})", source, mode); |
| 1159 | + if !span && monitors.len() > 1 { |
| 1160 | + // Per-monitor mode: scale wallpaper to each monitor individually |
| 1161 | + let compositor = Compositor::new(&monitors); |
| 1162 | + let wallpapers = Compositor::create_wallpapers_uniform(&monitors, image, mode); |
| 1163 | + let composited = compositor.composite(&wallpapers); |
| 1164 | + self.conn_mut()?.set_wallpaper(&composited)?; |
| 1165 | + |
| 1166 | + tracing::info!( |
| 1167 | + "Wallpaper set on {} monitors: {} (mode: {})", |
| 1168 | + monitors.len(), |
| 1169 | + source, |
| 1170 | + mode |
| 1171 | + ); |
| 1172 | + } else { |
| 1173 | + // Span mode or single monitor: scale to full screen |
| 1174 | + let conn = self.conn_mut()?; |
| 1175 | + let (width, height) = conn.screen_dimensions(); |
| 1176 | + let scaled = scale_image(image, width as u32, height as u32, mode); |
| 1177 | + conn.set_wallpaper(&scaled)?; |
| 1178 | + |
| 1179 | + tracing::info!("Wallpaper set: {} (mode: {})", source, mode); |
| 1180 | + } |
| 1154 | 1181 | |
| 1155 | 1182 | Ok(()) |
| 1156 | 1183 | } |
@@ -1262,7 +1289,8 @@ impl Daemon { |
| 1262 | 1289 | |
| 1263 | 1290 | if let Some(config) = ws_config { |
| 1264 | 1291 | let mode = config.mode.unwrap_or(self.state.config.general.mode); |
| 1265 | | - self.set_wallpaper_from_source(&config.source, mode, false)?; |
| 1292 | + // Default to per-monitor (span = false) |
| 1293 | + self.set_wallpaper_from_source(&config.source, mode, false, false)?; |
| 1266 | 1294 | } |
| 1267 | 1295 | |
| 1268 | 1296 | Ok(()) |