@@ -266,7 +266,7 @@ impl App { |
| 266 | return; | 266 | return; |
| 267 | } | 267 | } |
| 268 | | 268 | |
| 269 | - // Update monitor positions from profile | 269 | + // Update monitor positions and settings from profile |
| 270 | for state in view.monitors_mut() { | 270 | for state in view.monitors_mut() { |
| 271 | if let Some(config) = config_map.get(state.info.name.as_str()) { | 271 | if let Some(config) = config_map.get(state.info.name.as_str()) { |
| 272 | // Sanity check: don't apply positions that are clearly wrong | 272 | // Sanity check: don't apply positions that are clearly wrong |
@@ -287,6 +287,27 @@ impl App { |
| 287 | state.info.name | 287 | state.info.name |
| 288 | ); | 288 | ); |
| 289 | } | 289 | } |
| | 290 | + |
| | 291 | + // Apply mode dimensions and scaling from profile |
| | 292 | + state.mode_width = config.width; |
| | 293 | + state.mode_height = config.height; |
| | 294 | + state.refresh = config.refresh; |
| | 295 | + state.rotation = config.rotation; |
| | 296 | + state.scale = config.scale; |
| | 297 | + state.enabled = config.enabled; |
| | 298 | + |
| | 299 | + // Update visual rect to effective (scaled) dimensions |
| | 300 | + let (rot_w, rot_h) = match config.rotation { |
| | 301 | + 90 | 270 => (config.height, config.width), |
| | 302 | + _ => (config.width, config.height), |
| | 303 | + }; |
| | 304 | + if (config.scale - 1.0).abs() < 0.001 { |
| | 305 | + state.info.rect.width = rot_w; |
| | 306 | + state.info.rect.height = rot_h; |
| | 307 | + } else { |
| | 308 | + state.info.rect.width = (rot_w as f64 / config.scale).round() as u32; |
| | 309 | + state.info.rect.height = (rot_h as f64 / config.scale).round() as u32; |
| | 310 | + } |
| 290 | } | 311 | } |
| 291 | } | 312 | } |
| 292 | | 313 | |
@@ -581,6 +602,59 @@ impl App { |
| 581 | } | 602 | } |
| 582 | } | 603 | } |
| 583 | | 604 | |
| | 605 | + /// Refit the window to the current screen after a display change. |
| | 606 | + /// Uses root window geometry (which reflects the effective screen size after |
| | 607 | + /// transforms/scaling) rather than monitor detection (which returns raw mode sizes). |
| | 608 | + fn refit_window(&mut self) { |
| | 609 | + // Sync to ensure screen resize has been processed by the X server |
| | 610 | + let _ = self.conn.sync(); |
| | 611 | + |
| | 612 | + // Query root window geometry for actual effective screen dimensions |
| | 613 | + let (scr_w, scr_h) = match self.conn.inner().get_geometry(self.conn.root()) { |
| | 614 | + Ok(cookie) => match cookie.reply() { |
| | 615 | + Ok(geom) => (geom.width as u32, geom.height as u32), |
| | 616 | + Err(_) => return, |
| | 617 | + }, |
| | 618 | + Err(_) => return, |
| | 619 | + }; |
| | 620 | + |
| | 621 | + let win_w = WINDOW_WIDTH.min(scr_w.saturating_sub(40)); |
| | 622 | + let win_h = WINDOW_HEIGHT.min(scr_h.saturating_sub(40)); |
| | 623 | + let x = (scr_w as i32 - win_w as i32) / 2; |
| | 624 | + let y = (scr_h as i32 - win_h as i32) / 2; |
| | 625 | + |
| | 626 | + tracing::info!( |
| | 627 | + "refit_window: screen={}x{}, requesting window {}x{} at ({}, {})", |
| | 628 | + scr_w, scr_h, win_w, win_h, x, y |
| | 629 | + ); |
| | 630 | + |
| | 631 | + if let Err(e) = self.window.set_geometry(Rect::new(x, y, win_w, win_h)) { |
| | 632 | + tracing::warn!("failed to refit window: {}", e); |
| | 633 | + return; |
| | 634 | + } |
| | 635 | + |
| | 636 | + // Flush and sync so the WM processes the configure request |
| | 637 | + let _ = self.conn.flush(); |
| | 638 | + let _ = self.conn.sync(); |
| | 639 | + |
| | 640 | + // Query ACTUAL window geometry (WM may have adjusted our request) |
| | 641 | + let (actual_w, actual_h) = match self.conn.inner().get_geometry(self.window.id()) { |
| | 642 | + Ok(cookie) => match cookie.reply() { |
| | 643 | + Ok(geom) => (geom.width as u32, geom.height as u32), |
| | 644 | + Err(_) => (win_w, win_h), |
| | 645 | + }, |
| | 646 | + Err(_) => (win_w, win_h), |
| | 647 | + }; |
| | 648 | + |
| | 649 | + tracing::info!( |
| | 650 | + "refit_window: actual window size after WM = {}x{}", |
| | 651 | + actual_w, actual_h |
| | 652 | + ); |
| | 653 | + |
| | 654 | + // Update renderer and layout to match actual window size |
| | 655 | + self.handle_resize(Size::new(actual_w, actual_h)); |
| | 656 | + } |
| | 657 | + |
| 584 | /// Update the cursor based on monitor view state. | 658 | /// Update the cursor based on monitor view state. |
| 585 | fn update_cursor(&mut self) { | 659 | fn update_cursor(&mut self) { |
| 586 | let shape = self.monitor_view.cursor_shape(); | 660 | let shape = self.monitor_view.cursor_shape(); |
@@ -635,11 +709,13 @@ impl App { |
| 635 | output.map(|o| o.modes.len()).unwrap_or(0) | 709 | output.map(|o| o.modes.len()).unwrap_or(0) |
| 636 | ); | 710 | ); |
| 637 | | 711 | |
| | 712 | + // Pass mode_width/mode_height (raw resolution) to display panel so |
| | 713 | + // the resolution dropdown shows actual hardware modes, not scaled values. |
| 638 | self.display_panel.set_selected_monitor( | 714 | self.display_panel.set_selected_monitor( |
| 639 | Some(&state.info.name), | 715 | Some(&state.info.name), |
| 640 | output, | 716 | output, |
| 641 | - state.info.rect.width, | 717 | + state.mode_width, |
| 642 | - state.info.rect.height, | 718 | + state.mode_height, |
| 643 | state.refresh, | 719 | state.refresh, |
| 644 | state.rotation, | 720 | state.rotation, |
| 645 | state.scale, | 721 | state.scale, |
@@ -656,9 +732,6 @@ impl App { |
| 656 | let randr = self.randr.as_ref()?; | 732 | let randr = self.randr.as_ref()?; |
| 657 | let outputs = randr.get_outputs().ok()?; | 733 | let outputs = randr.get_outputs().ok()?; |
| 658 | | 734 | |
| 659 | - // Capture current DPI scale | | |
| 660 | - let current_scale = dpi::get_current_scale(); | | |
| 661 | - | | |
| 662 | Some( | 735 | Some( |
| 663 | outputs | 736 | outputs |
| 664 | .iter() | 737 | .iter() |
@@ -666,6 +739,11 @@ impl App { |
| 666 | .map(|o| { | 739 | .map(|o| { |
| 667 | let mode = o.current_mode.as_ref().unwrap(); | 740 | let mode = o.current_mode.as_ref().unwrap(); |
| 668 | let pos = o.position.unwrap_or((0, 0)); | 741 | let pos = o.position.unwrap_or((0, 0)); |
| | 742 | + // Read per-monitor scale from CRTC transform |
| | 743 | + let scale = o |
| | 744 | + .crtc |
| | 745 | + .map(|c| randr.get_crtc_scale(c)) |
| | 746 | + .unwrap_or(1.0); |
| 669 | MonitorConfig { | 747 | MonitorConfig { |
| 670 | name: o.name.clone(), | 748 | name: o.name.clone(), |
| 671 | enabled: true, | 749 | enabled: true, |
@@ -674,8 +752,8 @@ impl App { |
| 674 | width: mode.width as u32, | 752 | width: mode.width as u32, |
| 675 | height: mode.height as u32, | 753 | height: mode.height as u32, |
| 676 | refresh: mode.refresh, | 754 | refresh: mode.refresh, |
| 677 | - scale: current_scale, // Capture current DPI scale | 755 | + scale, |
| 678 | - rotation: 0, // TODO: capture actual rotation | 756 | + rotation: 0, // TODO: capture actual rotation |
| 679 | } | 757 | } |
| 680 | }) | 758 | }) |
| 681 | .collect(), | 759 | .collect(), |
@@ -698,6 +776,8 @@ impl App { |
| 698 | self.pre_change_config = self.capture_current_randr_state(); | 776 | self.pre_change_config = self.capture_current_randr_state(); |
| 699 | | 777 | |
| 700 | // Build MonitorConfig from current view state | 778 | // Build MonitorConfig from current view state |
| | 779 | + // Use mode_width/mode_height (raw hardware resolution) for the RandR mode, |
| | 780 | + // not info.rect which holds effective (scaled) dimensions. |
| 701 | let primary_name = self.monitor_view.primary_name().map(|s| s.to_string()); | 781 | let primary_name = self.monitor_view.primary_name().map(|s| s.to_string()); |
| 702 | let configs: Vec<MonitorConfig> = self | 782 | let configs: Vec<MonitorConfig> = self |
| 703 | .monitor_view | 783 | .monitor_view |
@@ -708,22 +788,21 @@ impl App { |
| 708 | enabled: state.enabled, | 788 | enabled: state.enabled, |
| 709 | x: state.real_position.x, | 789 | x: state.real_position.x, |
| 710 | y: state.real_position.y, | 790 | y: state.real_position.y, |
| 711 | - width: state.info.rect.width, | 791 | + width: state.mode_width, |
| 712 | - height: state.info.rect.height, | 792 | + height: state.mode_height, |
| 713 | refresh: state.refresh, | 793 | refresh: state.refresh, |
| 714 | scale: state.scale, | 794 | scale: state.scale, |
| 715 | rotation: state.rotation, | 795 | rotation: state.rotation, |
| 716 | }) | 796 | }) |
| 717 | .collect(); | 797 | .collect(); |
| 718 | | 798 | |
| 719 | - // IMPORTANT: Prepare the screen size BEFORE applying any configurations | 799 | + // Apply CRTCs using the same approach as xrandr: |
| 720 | - // This is essential for rotation changes which may require a larger virtual screen | 800 | + // 1. Apply each CRTC (ensure_screen_size inside apply_monitor grows if needed) |
| 721 | - if let Err(e) = randr.prepare_screen_for_configs(&configs) { | 801 | + // 2. Shrink the screen to fit effective dimensions after all CRTCs are applied |
| 722 | - tracing::error!("failed to prepare screen size: {}", e); | 802 | + // |
| 723 | - self.set_status(&format!("Failed to prepare screen: {}", e)); | 803 | + // When scaling down (e.g., 2x), the CRTC transform reduces the scanout size |
| 724 | - return; | 804 | + // (2880x1800 mode at 2x → 1440x900 scanout), so it always fits within the |
| 725 | - } | 805 | + // current screen. The screen is then shrunk to match after all CRTCs are set. |
| 726 | - | | |
| 727 | let mut success_count = 0; | 806 | let mut success_count = 0; |
| 728 | let mut error_count = 0; | 807 | let mut error_count = 0; |
| 729 | | 808 | |
@@ -737,6 +816,13 @@ impl App { |
| 737 | } | 816 | } |
| 738 | } | 817 | } |
| 739 | | 818 | |
| | 819 | + // Shrink the screen to fit the effective (post-transform) dimensions. |
| | 820 | + // This must happen AFTER all CRTCs are applied so the server knows the |
| | 821 | + // actual scanout sizes. This matches xrandr's behavior. |
| | 822 | + if let Err(e) = randr.shrink_screen_to_fit() { |
| | 823 | + tracing::error!("failed to shrink screen: {}", e); |
| | 824 | + } |
| | 825 | + |
| 740 | // Set primary | 826 | // Set primary |
| 741 | if let Some(ref name) = primary_name { | 827 | if let Some(ref name) = primary_name { |
| 742 | if let Err(e) = randr.set_primary(name) { | 828 | if let Err(e) = randr.set_primary(name) { |
@@ -748,13 +834,7 @@ impl App { |
| 748 | tracing::error!("failed to flush: {}", e); | 834 | tracing::error!("failed to flush: {}", e); |
| 749 | } | 835 | } |
| 750 | | 836 | |
| 751 | - // Try to shrink screen to fit (non-fatal if it fails) | | |
| 752 | - if let Err(e) = randr.shrink_screen_to_fit() { | | |
| 753 | - tracing::debug!("shrink_screen_to_fit failed (non-fatal): {}", e); | | |
| 754 | - } | | |
| 755 | - | | |
| 756 | // Apply DPI scaling if any monitor has non-1.0 scale | 837 | // Apply DPI scaling if any monitor has non-1.0 scale |
| 757 | - // Use the primary monitor's scale, or the first enabled monitor | | |
| 758 | let scale = configs | 838 | let scale = configs |
| 759 | .iter() | 839 | .iter() |
| 760 | .find(|c| c.enabled && primary_name.as_ref() == Some(&c.name)) | 840 | .find(|c| c.enabled && primary_name.as_ref() == Some(&c.name)) |
@@ -764,7 +844,6 @@ impl App { |
| 764 | | 844 | |
| 765 | if let Err(e) = dpi::apply_dpi_scale(scale) { | 845 | if let Err(e) = dpi::apply_dpi_scale(scale) { |
| 766 | tracing::error!("failed to apply DPI scale: {}", e); | 846 | tracing::error!("failed to apply DPI scale: {}", e); |
| 767 | - // Non-fatal - continue with the rest | | |
| 768 | } | 847 | } |
| 769 | | 848 | |
| 770 | if error_count > 0 { | 849 | if error_count > 0 { |
@@ -793,7 +872,10 @@ impl App { |
| 793 | } | 872 | } |
| 794 | } | 873 | } |
| 795 | | 874 | |
| 796 | - // Show confirmation overlay | 875 | + // Refit window to the new screen dimensions before showing overlay |
| | 876 | + self.refit_window(); |
| | 877 | + |
| | 878 | + // Show confirmation overlay (uses current window size after refit) |
| 797 | let size = self.window.size(); | 879 | let size = self.window.size(); |
| 798 | let window_rect = Rect::new(0, 0, size.width, size.height); | 880 | let window_rect = Rect::new(0, 0, size.width, size.height); |
| 799 | self.confirm_overlay = Some(ConfirmOverlay::new(window_rect)); | 881 | self.confirm_overlay = Some(ConfirmOverlay::new(window_rect)); |
@@ -809,7 +891,10 @@ impl App { |
| 809 | watchdog::cancel_watchdog(); | 891 | watchdog::cancel_watchdog(); |
| 810 | self.watchdog_child = None; | 892 | self.watchdog_child = None; |
| 811 | | 893 | |
| 812 | - // Update original_monitors to current state | 894 | + // Update original_monitors to current state. |
| | 895 | + // Use mode_width/mode_height (raw hardware resolution) so that revert_layout |
| | 896 | + // can find the correct RandR mode. info.rect holds effective (scaled) dimensions |
| | 897 | + // which may not correspond to an actual mode. |
| 813 | let primary_name = self.monitor_view.primary_name().map(|s| s.to_string()); | 898 | let primary_name = self.monitor_view.primary_name().map(|s| s.to_string()); |
| 814 | self.original_monitors = self | 899 | self.original_monitors = self |
| 815 | .monitor_view | 900 | .monitor_view |
@@ -820,8 +905,8 @@ impl App { |
| 820 | rect: Rect::new( | 905 | rect: Rect::new( |
| 821 | state.real_position.x, | 906 | state.real_position.x, |
| 822 | state.real_position.y, | 907 | state.real_position.y, |
| 823 | - state.info.rect.width, | 908 | + state.mode_width, |
| 824 | - state.info.rect.height, | 909 | + state.mode_height, |
| 825 | ), | 910 | ), |
| 826 | primary: primary_name.as_ref() == Some(&state.info.name), | 911 | primary: primary_name.as_ref() == Some(&state.info.name), |
| 827 | width_mm: state.info.width_mm, | 912 | width_mm: state.info.width_mm, |
@@ -829,6 +914,7 @@ impl App { |
| 829 | }) | 914 | }) |
| 830 | .collect(); | 915 | .collect(); |
| 831 | | 916 | |
| | 917 | + self.refit_window(); |
| 832 | self.set_status("Display settings confirmed"); | 918 | self.set_status("Display settings confirmed"); |
| 833 | self.monitor_view.clear_dirty(); | 919 | self.monitor_view.clear_dirty(); |
| 834 | } | 920 | } |
@@ -843,25 +929,15 @@ impl App { |
| 843 | | 929 | |
| 844 | if let Some(ref configs) = self.pre_change_config.take() { | 930 | if let Some(ref configs) = self.pre_change_config.take() { |
| 845 | if let Some(ref randr) = self.randr { | 931 | if let Some(ref randr) = self.randr { |
| 846 | - // IMPORTANT: Prepare screen size BEFORE reverting | 932 | + // Apply CRTCs (ensure_screen_size inside grows if needed), then shrink |
| 847 | - // The pre-change config may have different dimensions than current | | |
| 848 | - if let Err(e) = randr.prepare_screen_for_configs(configs) { | | |
| 849 | - tracing::error!("failed to prepare screen for revert: {}", e); | | |
| 850 | - // Continue anyway - we still want to try to revert | | |
| 851 | - } | | |
| 852 | - | | |
| 853 | for config in configs { | 933 | for config in configs { |
| 854 | if let Err(e) = randr.apply_monitor(config) { | 934 | if let Err(e) = randr.apply_monitor(config) { |
| 855 | tracing::error!("failed to revert {}: {}", config.name, e); | 935 | tracing::error!("failed to revert {}: {}", config.name, e); |
| 856 | } | 936 | } |
| 857 | } | 937 | } |
| | 938 | + let _ = randr.shrink_screen_to_fit(); |
| 858 | let _ = randr.flush(); | 939 | let _ = randr.flush(); |
| 859 | | 940 | |
| 860 | - // Try to shrink screen to fit | | |
| 861 | - if let Err(e) = randr.shrink_screen_to_fit() { | | |
| 862 | - tracing::debug!("shrink_screen_to_fit failed during revert: {}", e); | | |
| 863 | - } | | |
| 864 | - | | |
| 865 | // Restore DPI scale from pre-change config | 941 | // Restore DPI scale from pre-change config |
| 866 | let scale = configs | 942 | let scale = configs |
| 867 | .iter() | 943 | .iter() |
@@ -876,6 +952,7 @@ impl App { |
| 876 | | 952 | |
| 877 | // Reset view to original monitors | 953 | // Reset view to original monitors |
| 878 | self.monitor_view.set_monitors(self.original_monitors.clone()); | 954 | self.monitor_view.set_monitors(self.original_monitors.clone()); |
| | 955 | + self.refit_window(); |
| 879 | self.set_status("Display settings reverted"); | 956 | self.set_status("Display settings reverted"); |
| 880 | } | 957 | } |
| 881 | | 958 | |
@@ -891,10 +968,12 @@ impl App { |
| 891 | // Restore original monitors in view | 968 | // Restore original monitors in view |
| 892 | self.monitor_view.set_monitors(self.original_monitors.clone()); | 969 | self.monitor_view.set_monitors(self.original_monitors.clone()); |
| 893 | | 970 | |
| 894 | - // Apply via RandR | 971 | + // Apply via RandR using disable→resize→apply pattern |
| 895 | if let Some(ref randr) = self.randr { | 972 | if let Some(ref randr) = self.randr { |
| 896 | - for m in &self.original_monitors { | 973 | + let configs: Vec<MonitorConfig> = self |
| 897 | - let config = MonitorConfig { | 974 | + .original_monitors |
| | 975 | + .iter() |
| | 976 | + .map(|m| MonitorConfig { |
| 898 | name: m.name.clone(), | 977 | name: m.name.clone(), |
| 899 | enabled: true, | 978 | enabled: true, |
| 900 | x: m.rect.x, | 979 | x: m.rect.x, |
@@ -904,12 +983,16 @@ impl App { |
| 904 | refresh: 60.0, | 983 | refresh: 60.0, |
| 905 | scale: 1.0, | 984 | scale: 1.0, |
| 906 | rotation: 0, | 985 | rotation: 0, |
| 907 | - }; | 986 | + }) |
| | 987 | + .collect(); |
| 908 | | 988 | |
| 909 | - if let Err(e) = randr.apply_monitor(&config) { | 989 | + // Apply CRTCs (ensure_screen_size inside grows if needed), then shrink |
| 910 | - tracing::error!("failed to revert {}: {}", m.name, e); | 990 | + for config in &configs { |
| | 991 | + if let Err(e) = randr.apply_monitor(config) { |
| | 992 | + tracing::error!("failed to revert {}: {}", config.name, e); |
| 911 | } | 993 | } |
| 912 | } | 994 | } |
| | 995 | + let _ = randr.shrink_screen_to_fit(); |
| 913 | | 996 | |
| 914 | // Restore primary | 997 | // Restore primary |
| 915 | if let Some(m) = self.original_monitors.iter().find(|m| m.primary) { | 998 | if let Some(m) = self.original_monitors.iter().find(|m| m.primary) { |
@@ -921,6 +1004,7 @@ impl App { |
| 921 | let _ = randr.flush(); | 1004 | let _ = randr.flush(); |
| 922 | } | 1005 | } |
| 923 | | 1006 | |
| | 1007 | + self.refit_window(); |
| 924 | self.set_status("Reverted to original layout"); | 1008 | self.set_status("Reverted to original layout"); |
| 925 | } | 1009 | } |
| 926 | | 1010 | |
@@ -934,21 +1018,21 @@ impl App { |
| 934 | fn save_profile(&mut self) { | 1018 | fn save_profile(&mut self) { |
| 935 | let primary_name = self.monitor_view.primary_name().map(|s| s.to_string()); | 1019 | let primary_name = self.monitor_view.primary_name().map(|s| s.to_string()); |
| 936 | | 1020 | |
| 937 | - // Build profile from current layout | 1021 | + // Build profile from current layout using raw mode dimensions |
| 938 | let monitors: Vec<MonitorConfig> = self | 1022 | let monitors: Vec<MonitorConfig> = self |
| 939 | .monitor_view | 1023 | .monitor_view |
| 940 | .monitors() | 1024 | .monitors() |
| 941 | .iter() | 1025 | .iter() |
| 942 | .map(|state| MonitorConfig { | 1026 | .map(|state| MonitorConfig { |
| 943 | name: state.info.name.clone(), | 1027 | name: state.info.name.clone(), |
| 944 | - enabled: true, | 1028 | + enabled: state.enabled, |
| 945 | x: state.real_position.x, | 1029 | x: state.real_position.x, |
| 946 | y: state.real_position.y, | 1030 | y: state.real_position.y, |
| 947 | - width: state.info.rect.width, | 1031 | + width: state.mode_width, |
| 948 | - height: state.info.rect.height, | 1032 | + height: state.mode_height, |
| 949 | - refresh: 60.0, | 1033 | + refresh: state.refresh, |
| 950 | - scale: 1.0, | 1034 | + scale: state.scale, |
| 951 | - rotation: 0, | 1035 | + rotation: state.rotation, |
| 952 | }) | 1036 | }) |
| 953 | .collect(); | 1037 | .collect(); |
| 954 | | 1038 | |
@@ -984,14 +1068,14 @@ impl App { |
| 984 | .iter() | 1068 | .iter() |
| 985 | .map(|state| MonitorConfig { | 1069 | .map(|state| MonitorConfig { |
| 986 | name: state.info.name.clone(), | 1070 | name: state.info.name.clone(), |
| 987 | - enabled: true, | 1071 | + enabled: state.enabled, |
| 988 | x: state.real_position.x, | 1072 | x: state.real_position.x, |
| 989 | y: state.real_position.y, | 1073 | y: state.real_position.y, |
| 990 | - width: state.info.rect.width, | 1074 | + width: state.mode_width, |
| 991 | - height: state.info.rect.height, | 1075 | + height: state.mode_height, |
| 992 | - refresh: 60.0, | 1076 | + refresh: state.refresh, |
| 993 | - scale: 1.0, | 1077 | + scale: state.scale, |
| 994 | - rotation: 0, | 1078 | + rotation: state.rotation, |
| 995 | }) | 1079 | }) |
| 996 | .collect(); | 1080 | .collect(); |
| 997 | | 1081 | |