feat: add KWin/KDE Plasma screen capture via xdg-desktop-portal ScreenCast + PipeWire
Add a second capture backend for compositors without wlr-screencopy (KWin, GNOME, etc.) using the xdg-desktop-portal ScreenCast interface and PipeWire DMA-BUF streaming. New files: - src/backend_detect.rs: auto-detect wlr-screencopy vs portal backend - src/cap_portal.rs: Portal session setup + PipeWire DMA-BUF thread - src/state_portal.rs: StatePortal encoder pipeline (DMA-BUF → VAAPI) Changes: - Cargo.toml: add ashpd 0.13, tokio 1, pipewire 0.9, libspa 0.9, crossbeam-channel 0.5 - src/args.rs: add --backend CLI flag - src/avhw.rs: extract create_encoder() from inline State code - src/main.rs: route to portal or wlr-screencopy based on backend - src/state.rs: fix params.destroy() on dup failure, cleanup in_flight_surface on copy fail, use create_encoder() - tests/integration_test.rs: add --backend flag tests
This commit is contained in:
184
src/transform.rs
184
src/transform.rs
@@ -56,12 +56,7 @@ pub fn transform_basis(transform: Transform) -> (i32, i32, i32, i32) {
|
||||
/// new_x = a * x + b * y + offset_x
|
||||
/// new_y = c * x + d * y + offset_y
|
||||
/// ```
|
||||
pub fn screen_to_frame(
|
||||
transform: Transform,
|
||||
rect: Rect,
|
||||
frame_w: i32,
|
||||
frame_h: i32,
|
||||
) -> Rect {
|
||||
pub fn screen_to_frame(transform: Transform, rect: Rect, frame_w: i32, frame_h: i32) -> Rect {
|
||||
let (a, b, c, d) = transform_basis(transform);
|
||||
|
||||
// Compute the offset so that the transformed origin maps correctly.
|
||||
@@ -90,9 +85,10 @@ pub fn screen_to_frame(
|
||||
/// and `(w, h)` unchanged otherwise.
|
||||
pub fn transpose_if_transform_transposed(transform: Transform, w: i32, h: i32) -> (i32, i32) {
|
||||
match transform {
|
||||
Transform::Normal90 | Transform::Normal270 | Transform::Flipped90 | Transform::Flipped270 => {
|
||||
(h, w)
|
||||
}
|
||||
Transform::Normal90
|
||||
| Transform::Normal270
|
||||
| Transform::Flipped90
|
||||
| Transform::Flipped270 => (h, w),
|
||||
_ => (w, h),
|
||||
}
|
||||
}
|
||||
@@ -161,15 +157,33 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn screen_to_frame_identity_unchanged() {
|
||||
let rect = Rect { x: 10, y: 20, w: 100, h: 50 };
|
||||
let rect = Rect {
|
||||
x: 10,
|
||||
y: 20,
|
||||
w: 100,
|
||||
h: 50,
|
||||
};
|
||||
let result = screen_to_frame(Transform::Normal, rect, 1920, 1080);
|
||||
assert_eq!(result, Rect { x: 10, y: 20, w: 100, h: 50 });
|
||||
assert_eq!(
|
||||
result,
|
||||
Rect {
|
||||
x: 10,
|
||||
y: 20,
|
||||
w: 100,
|
||||
h: 50
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_to_frame_90_rotates_origin() {
|
||||
// 90° CW: top-left (0,0) in screen should map to bottom-left in frame
|
||||
let rect = Rect { x: 0, y: 0, w: 100, h: 50 };
|
||||
let rect = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 50,
|
||||
};
|
||||
let result = screen_to_frame(Transform::Normal90, rect, 1080, 1920);
|
||||
// a=0,b=1,c=-1,d=0 => offset_x=0, offset_y=1920 (c+d=-1<0)
|
||||
// new_x = 0*0 + 1*0 + 0 = 0
|
||||
@@ -183,7 +197,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn screen_to_frame_180_rotates() {
|
||||
let rect = Rect { x: 100, y: 200, w: 300, h: 400 };
|
||||
let rect = Rect {
|
||||
x: 100,
|
||||
y: 200,
|
||||
w: 300,
|
||||
h: 400,
|
||||
};
|
||||
let result = screen_to_frame(Transform::Normal180, rect, 1920, 1080);
|
||||
// a=-1,b=0,c=0,d=-1, offset_x=1920, offset_y=1080
|
||||
assert_eq!(result.x, -100 + 1920);
|
||||
@@ -194,7 +213,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn screen_to_frame_flipped_horizontal() {
|
||||
let rect = Rect { x: 50, y: 30, w: 200, h: 100 };
|
||||
let rect = Rect {
|
||||
x: 50,
|
||||
y: 30,
|
||||
w: 200,
|
||||
h: 100,
|
||||
};
|
||||
let result = screen_to_frame(Transform::Flipped, rect, 1920, 1080);
|
||||
// a=-1,b=0,c=0,d=1, offset_x=1920, offset_y=0
|
||||
assert_eq!(result.x, -50 + 1920);
|
||||
@@ -207,85 +231,179 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn transpose_normal_no_swap() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Normal, 1920, 1080), (1920, 1080));
|
||||
assert_eq!(
|
||||
transpose_if_transform_transposed(Transform::Normal, 1920, 1080),
|
||||
(1920, 1080)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_90_swaps() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Normal90, 1920, 1080), (1080, 1920));
|
||||
assert_eq!(
|
||||
transpose_if_transform_transposed(Transform::Normal90, 1920, 1080),
|
||||
(1080, 1920)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_180_no_swap() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Normal180, 1920, 1080), (1920, 1080));
|
||||
assert_eq!(
|
||||
transpose_if_transform_transposed(Transform::Normal180, 1920, 1080),
|
||||
(1920, 1080)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_270_swaps() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Normal270, 1920, 1080), (1080, 1920));
|
||||
assert_eq!(
|
||||
transpose_if_transform_transposed(Transform::Normal270, 1920, 1080),
|
||||
(1080, 1920)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_flipped_no_swap() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Flipped, 1920, 1080), (1920, 1080));
|
||||
assert_eq!(
|
||||
transpose_if_transform_transposed(Transform::Flipped, 1920, 1080),
|
||||
(1920, 1080)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_flipped90_swaps() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Flipped90, 1920, 1080), (1080, 1920));
|
||||
assert_eq!(
|
||||
transpose_if_transform_transposed(Transform::Flipped90, 1920, 1080),
|
||||
(1080, 1920)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_flipped180_no_swap() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Flipped180, 1920, 1080), (1920, 1080));
|
||||
assert_eq!(
|
||||
transpose_if_transform_transposed(Transform::Flipped180, 1920, 1080),
|
||||
(1920, 1080)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_flipped270_swaps() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Flipped270, 1920, 1080), (1080, 1920));
|
||||
assert_eq!(
|
||||
transpose_if_transform_transposed(Transform::Flipped270, 1920, 1080),
|
||||
(1080, 1920)
|
||||
);
|
||||
}
|
||||
|
||||
// ── fit_inside_bounds ─────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn fit_inside_already_fits() {
|
||||
let rect = Rect { x: 10, y: 20, w: 100, h: 50 };
|
||||
let rect = Rect {
|
||||
x: 10,
|
||||
y: 20,
|
||||
w: 100,
|
||||
h: 50,
|
||||
};
|
||||
let result = fit_inside_bounds(rect, 1920, 1080);
|
||||
assert_eq!(result, rect);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_inside_clips_right_and_bottom() {
|
||||
let rect = Rect { x: 1800, y: 1000, w: 200, h: 200 };
|
||||
let rect = Rect {
|
||||
x: 1800,
|
||||
y: 1000,
|
||||
w: 200,
|
||||
h: 200,
|
||||
};
|
||||
let result = fit_inside_bounds(rect, 1920, 1080);
|
||||
assert_eq!(result, Rect { x: 1800, y: 1000, w: 120, h: 80 });
|
||||
assert_eq!(
|
||||
result,
|
||||
Rect {
|
||||
x: 1800,
|
||||
y: 1000,
|
||||
w: 120,
|
||||
h: 80
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_inside_clips_negative_origin() {
|
||||
let rect = Rect { x: -50, y: -30, w: 200, h: 200 };
|
||||
let rect = Rect {
|
||||
x: -50,
|
||||
y: -30,
|
||||
w: 200,
|
||||
h: 200,
|
||||
};
|
||||
let result = fit_inside_bounds(rect, 1920, 1080);
|
||||
assert_eq!(result, Rect { x: 0, y: 0, w: 150, h: 170 });
|
||||
assert_eq!(
|
||||
result,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 150,
|
||||
h: 170
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_inside_completely_out_of_bounds() {
|
||||
let rect = Rect { x: 2000, y: 2000, w: 100, h: 100 };
|
||||
let rect = Rect {
|
||||
x: 2000,
|
||||
y: 2000,
|
||||
w: 100,
|
||||
h: 100,
|
||||
};
|
||||
let result = fit_inside_bounds(rect, 1920, 1080);
|
||||
assert_eq!(result, Rect { x: 1920, y: 1080, w: 0, h: 0 });
|
||||
assert_eq!(
|
||||
result,
|
||||
Rect {
|
||||
x: 1920,
|
||||
y: 1080,
|
||||
w: 0,
|
||||
h: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_inside_zero_size_rect() {
|
||||
let rect = Rect { x: 100, y: 100, w: 0, h: 0 };
|
||||
let rect = Rect {
|
||||
x: 100,
|
||||
y: 100,
|
||||
w: 0,
|
||||
h: 0,
|
||||
};
|
||||
let result = fit_inside_bounds(rect, 1920, 1080);
|
||||
assert_eq!(result, Rect { x: 100, y: 100, w: 0, h: 0 });
|
||||
assert_eq!(
|
||||
result,
|
||||
Rect {
|
||||
x: 100,
|
||||
y: 100,
|
||||
w: 0,
|
||||
h: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_inside_zero_bounds() {
|
||||
let rect = Rect { x: 0, y: 0, w: 100, h: 100 };
|
||||
let rect = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
};
|
||||
let result = fit_inside_bounds(rect, 0, 0);
|
||||
assert_eq!(result, Rect { x: 0, y: 0, w: 0, h: 0 });
|
||||
assert_eq!(
|
||||
result,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 0,
|
||||
h: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user