Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions apps/desktop-gpui/src/teleprompter_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ pub fn advance_playback_position(
.min(maximum_scroll.max(0.))
}

/// `playbackPosition = element.scrollTop`, with `if (element.scrollTop >=
/// maximumScroll - 1) element.scrollTop = 0`: playback resumes from where the
/// script actually is (a pause, or a wheel scroll between two plays, moves
/// gpui's offset, not ours), and only restarts from the top once it reached the
/// end. `offset_y` is gpui's scroll offset, which is negative.
pub fn playback_resume_position(offset_y: f32, maximum_scroll: f32) -> f32 {
let position = (-offset_y).clamp(0., maximum_scroll.max(0.));
if position >= maximum_scroll - 1. {
0.
} else {
position
}
}

// ---------------------------------------------------------------------------

/// Which range is being dragged, so one drag layer serves both pills.
Expand Down Expand Up @@ -406,13 +420,8 @@ impl TeleprompterWindow {
if !self.has_script() || maximum <= 1. {
return;
}
// `playbackPosition = element.scrollTop`: a wheel scroll between two
// plays moves gpui's offset, not ours, so playback resumes from where
// the script actually is.
self.position = (-f32::from(self.scroll.offset().y)).clamp(0., maximum);
// `if (element.scrollTop >= maximumScroll - 1) element.scrollTop = 0`
if self.position >= maximum - 1. {
self.position = 0.;
self.position = playback_resume_position(f32::from(self.scroll.offset().y), maximum);
if self.position == 0. {
self.apply_scroll();
}
self.settings_open = false;
Expand Down Expand Up @@ -1185,6 +1194,23 @@ mod tests {
assert_eq!(advance_playback_position(-5., 100., 0., 1.), 0.);
}

/// Pausing keeps the scroll offset, so pressing play again picks up where
/// the script stopped; only a script that already reached the end goes
/// back to the top (https://github.com/CapSoftware/Cap/issues/2081).
#[test]
fn playback_resumes_where_it_was_paused() {
let maximum = 1000.;
let mut position = playback_resume_position(0., maximum);
assert_eq!(position, 0.);
for _ in 0..90 {
position = advance_playback_position(position, maximum, 40., 1. / 30.);
}
assert!(position > 100.);
assert_eq!(playback_resume_position(-position, maximum), position);
assert_eq!(playback_resume_position(-(maximum - 0.5), maximum), 0.);
assert_eq!(playback_resume_position(25., maximum), 0.);
}

/// The ranges snap to their steps and stay inside their bounds, which is
/// what an `<input type="range" step="5">` does with a pointer anywhere on
/// the track.
Expand Down
53 changes: 53 additions & 0 deletions apps/desktop/src/routes/teleprompter-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,64 @@ import {
calculatePlaybackSpeed,
clamp,
countWords,
preserveScrollTop,
type TeleprompterPlaybackState,
teleprompterPlaybackRunning,
updateTeleprompterPlayback,
} from "./teleprompter-utils";

function fakeScroller(contentHeight: number, clientHeight: number) {
let scrollTop = 0;
const clampScrollTop = (value: number) =>
Math.max(0, Math.min(value, contentHeight - clientHeight));
return {
get scrollTop() {
return scrollTop;
},
set scrollTop(value: number) {
scrollTop = clampScrollTop(value);
},
layout(height: number) {
contentHeight = height;
scrollTop = clampScrollTop(scrollTop);
},
};
}

describe("teleprompter editor resizing", () => {
it("keeps the paused position when the editor collapses to measure itself", () => {
const scroller = fakeScroller(2000, 500);
scroller.scrollTop = 800;

preserveScrollTop(scroller, () => {
scroller.layout(400);
scroller.layout(2000);
});

expect(scroller.scrollTop).toBe(800);
});

it("lets the browser clamp a position the resized script no longer reaches", () => {
const scroller = fakeScroller(2000, 500);
scroller.scrollTop = 1400;

preserveScrollTop(scroller, () => {
scroller.layout(400);
scroller.layout(1200);
});

expect(scroller.scrollTop).toBe(700);
});

it("runs the resize without a scroller", () => {
let resized = false;
preserveScrollTop(undefined, () => {
resized = true;
});
expect(resized).toBe(true);
});
});

describe("teleprompter utilities", () => {
it("counts words in pasted scripts", () => {
expect(countWords(" One two\nthree ")).toBe(3);
Expand Down
13 changes: 13 additions & 0 deletions apps/desktop/src/routes/teleprompter-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ export function advancePlaybackPosition(
);
}

// Collapsing the auto-sized script editor to measure it briefly makes the
// scroller's content shorter than its viewport, and the browser clamps
// scrollTop to 0 during that forced layout. Resuming playback then started
// from the top: https://github.com/CapSoftware/Cap/issues/2081
export function preserveScrollTop(
container: { scrollTop: number } | undefined,
mutate: () => void,
) {
const scrollTop = container?.scrollTop;
mutate();
if (container && scrollTop !== undefined) container.scrollTop = scrollTop;
}

export type TeleprompterPlaybackState = {
requested: boolean;
recordingPaused: boolean;
Expand Down
7 changes: 5 additions & 2 deletions apps/desktop/src/routes/teleprompter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
calculatePlaybackSpeed,
clamp,
countWords,
preserveScrollTop,
type TeleprompterPlaybackState,
teleprompterPlaybackRunning,
updateTeleprompterPlayback,
Expand Down Expand Up @@ -300,8 +301,10 @@ export default function Teleprompter() {
function resizeEditor() {
const element = editorElement;
if (!element) return;
element.style.height = "0px";
element.style.height = `${element.scrollHeight}px`;
preserveScrollTop(scrollElement, () => {
element.style.height = "0px";
element.style.height = `${element.scrollHeight}px`;
});
}

function stopPlayback() {
Expand Down
Loading