Skip to content
Merged
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
1 change: 1 addition & 0 deletions apps/desktop-gpui/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

143 changes: 93 additions & 50 deletions apps/desktop-gpui/src/editor_sidebar/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ const CARD_GROUP: &str = "cursor-style-card";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CursorCard {
Default,
Family(CursorFamily),
Circle,
}

impl CursorCard {
fn label(self) -> &'static str {
match self {
Self::Default => "Default",
Self::Family(CursorFamily::MacOS) => "macOS",
Self::Family(CursorFamily::MacOSTahoe) => "macOS Tahoe",
Self::Family(CursorFamily::Windows) => "Windows",
Expand All @@ -29,6 +31,7 @@ impl CursorCard {

fn key(self) -> &'static str {
match self {
Self::Default => "auto",
Self::Family(CursorFamily::MacOS) => "macos",
Self::Family(CursorFamily::MacOSTahoe) => "tahoe",
Self::Family(CursorFamily::Windows) => "windows",
Expand All @@ -38,6 +41,7 @@ impl CursorCard {

fn cursor_type(self) -> CursorType {
match self {
Self::Default => CursorType::Auto,
Self::Family(CursorFamily::MacOS) => CursorType::MacOS,
Self::Family(CursorFamily::MacOSTahoe) => CursorType::MacOSTahoe,
Self::Family(CursorFamily::Windows) => CursorType::Windows,
Expand All @@ -46,16 +50,18 @@ impl CursorCard {
}
}

fn cursor_cards() -> [CursorCard; 4] {
fn cursor_cards() -> [CursorCard; 5] {
if cfg!(target_os = "windows") {
[
CursorCard::Default,
CursorCard::Family(CursorFamily::Windows),
CursorCard::Family(CursorFamily::MacOS),
CursorCard::Family(CursorFamily::MacOSTahoe),
CursorCard::Circle,
]
} else {
[
CursorCard::Default,
CursorCard::Family(CursorFamily::MacOS),
CursorCard::Family(CursorFamily::MacOSTahoe),
CursorCard::Family(CursorFamily::Windows),
Expand All @@ -64,13 +70,13 @@ fn cursor_cards() -> [CursorCard; 4] {
}
}

fn selected_card(cursor_type: &CursorType, recorded: Option<CursorFamily>) -> CursorCard {
fn selected_card(cursor_type: &CursorType) -> CursorCard {
if *cursor_type == CursorType::Circle {
return CursorCard::Circle;
}
match cursor_type.family() {
Some(family) => CursorCard::Family(family),
None => CursorCard::Family(recorded.unwrap_or(host_cursor_family())),
None => CursorCard::Default,
}
}

Expand Down Expand Up @@ -149,10 +155,7 @@ impl EditorWindow {
}

fn selected_cursor_card(&self) -> CursorCard {
selected_card(
self.style_control_project().cursor.cursor_type(),
self.recorded_cursor_family,
)
selected_card(self.style_control_project().cursor.cursor_type())
}

fn cursor_preview(&self, shape: CursorShape, size: f32) -> Option<Arc<RenderImage>> {
Expand Down Expand Up @@ -186,6 +189,26 @@ impl EditorWindow {
fn render_cursor_tile(&self, card: CursorCard, selected: bool, recorded: bool) -> AnyElement {
let theme = self.theme;
let art = match card {
CursorCard::Default => div()
.flex()
.items_center()
.gap(px(12.))
.child(
self.cursor_art(
self.recorded_cursor_family
.unwrap_or(host_cursor_family())
.arrow(),
ARROW_BOX,
),
)
.child(div().text_size(px(12.)).child("Default"))
.child(
div()
.text_size(px(11.))
.text_color(Hsla::from(theme.gray_11))
.child("Recorded cursors"),
)
.into_any_element(),
CursorCard::Family(family) => self.cursor_art(family.arrow(), ARROW_BOX),
CursorCard::Circle => circle_art(),
};
Expand Down Expand Up @@ -243,27 +266,29 @@ impl EditorWindow {
.gap(px(6.))
.cursor_pointer()
.child(self.render_cursor_tile(card, selected, is_recorded))
.child(
div()
.max_w_full()
.whitespace_nowrap()
.overflow_hidden()
.text_ellipsis()
.text_size(px(11.))
.line_height(px(11.))
.font_weight(FontWeight::MEDIUM)
.text_color(Hsla::from(if selected {
theme.gray_12
} else {
theme.gray_11
}))
.when(!selected, |this| {
this.group_hover(CARD_GROUP, |this| {
this.text_color(Hsla::from(theme.gray_12))
.when(card != CursorCard::Default, |this| {
this.child(
div()
.max_w_full()
.whitespace_nowrap()
.overflow_hidden()
.text_ellipsis()
.text_size(px(11.))
.line_height(px(11.))
.font_weight(FontWeight::MEDIUM)
.text_color(Hsla::from(if selected {
theme.gray_12
} else {
theme.gray_11
}))
.when(!selected, |this| {
this.group_hover(CARD_GROUP, |this| {
this.text_color(Hsla::from(theme.gray_12))
})
})
})
.child(card.label()),
)
.child(card.label()),
)
})
.on_click(cx.listener(move |this, _, window, cx| {
let cursor_type = cursor_type.clone();
this.edit_project("cursor-type", window, cx, move |project| {
Expand All @@ -281,14 +306,42 @@ impl EditorWindow {
let selected = self.selected_cursor_card();
let recorded = self.recorded_cursor_family;

let description = match selected {
CursorCard::Default => "Keeps the cursor shapes and appearance from your recording.",
CursorCard::Family(CursorFamily::MacOS) => {
"Classic macOS appearance. Custom cursors keep their recorded shape."
}
CursorCard::Family(CursorFamily::MacOSTahoe) => {
"macOS Tahoe appearance. Custom cursors keep their recorded shape."
}
CursorCard::Family(CursorFamily::Windows) => {
"Windows appearance. Custom cursors keep their recorded shape."
}
CursorCard::Circle => "Replaces all cursor shapes with a circle.",
};
div()
.flex()
.flex_row()
.flex_col()
.gap(px(CARD_GAP))
.children(
cursor_cards()
.into_iter()
.map(|card| self.render_cursor_card(card, selected == card, recorded, cx)),
.child(self.render_cursor_card(
CursorCard::Default,
selected == CursorCard::Default,
recorded,
cx,
))
.child(
div().flex().flex_row().gap(px(CARD_GAP)).children(
cursor_cards()
.into_iter()
.filter(|card| *card != CursorCard::Default)
.map(|card| self.render_cursor_card(card, selected == card, recorded, cx)),
),
)
.child(
div()
.text_size(px(11.))
.text_color(Hsla::from(self.theme.gray_11))
.child(description),
)
.into_any_element()
}
Expand Down Expand Up @@ -362,10 +415,11 @@ mod tests {
use super::*;

#[test]
fn cards_lead_with_the_host_family() {
fn cards_offer_default_before_the_host_family() {
let cards = cursor_cards();
assert_eq!(cards[0], CursorCard::Family(host_cursor_family()));
assert_eq!(cards[3], CursorCard::Circle);
assert_eq!(cards[0], CursorCard::Default);
assert_eq!(cards[1], CursorCard::Family(host_cursor_family()));
assert_eq!(cards[4], CursorCard::Circle);
for family in [
CursorFamily::MacOS,
CursorFamily::MacOSTahoe,
Expand All @@ -379,25 +433,14 @@ mod tests {
fn every_card_round_trips_through_its_type() {
for card in cursor_cards() {
let written = card.cursor_type();
assert_ne!(written, CursorType::Auto);
assert_eq!(selected_card(&written, None), card, "{:?}", card.label());
assert_eq!(selected_card(&written), card, "{:?}", card.label());
}
}

#[test]
fn auto_follows_the_recording_then_the_host() {
assert_eq!(
selected_card(&CursorType::Auto, Some(CursorFamily::MacOSTahoe)),
CursorCard::Family(CursorFamily::MacOSTahoe)
);
assert_eq!(
selected_card(&CursorType::Pointer, Some(CursorFamily::Windows)),
CursorCard::Family(CursorFamily::Windows)
);
assert_eq!(
selected_card(&CursorType::Auto, None),
CursorCard::Family(host_cursor_family())
);
fn auto_and_legacy_pointer_select_default() {
assert_eq!(selected_card(&CursorType::Auto), CursorCard::Default);
assert_eq!(selected_card(&CursorType::Pointer), CursorCard::Default);
}

#[test]
Expand Down
80 changes: 52 additions & 28 deletions apps/desktop/src/routes/editor/CursorStylePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ import tahoeArrow from "../../../../../crates/cursor-info/assets/mac/tahoe/defau
import windowsArrow from "../../../../../crates/cursor-info/assets/windows/arrow.svg?raw";
import { RgbInput } from "./color-utils";
import { type TransformedMeta, useEditorContext } from "./context";
import {
type CursorFamily,
type CursorStyle,
cursorStyleDescription,
cursorStyleOrder,
selectedCursorStyle,
} from "./cursor-style";
import { Field, Section, Slider } from "./ui";

export type CursorFamily = "macos" | "tahoe" | "windows";
type CursorStyle = CursorFamily | "circle";
export type { CursorFamily } from "./cursor-style";

const CURSOR_FAMILIES = {
macos: { label: "macOS", arrow: macArrow },
Expand Down Expand Up @@ -60,12 +66,6 @@ function hostCursorFamily(): CursorFamily {
return ostype() === "windows" ? "windows" : "macos";
}

function cursorStyleOrder(): CursorStyle[] {
return ostype() === "windows"
? ["windows", "macos", "tahoe", "circle"]
: ["macos", "tahoe", "windows", "circle"];
}

function CursorArrow(props: { svg: string }) {
return (
<div class="h-[34px] [&>svg]:h-full [&>svg]:w-auto" innerHTML={props.svg} />
Expand All @@ -78,31 +78,58 @@ function CircleCursor() {
);
}

function CursorStyleCard(props: { style: CursorStyle; recorded: boolean }) {
function CursorStyleCard(props: {
style: CursorStyle;
recorded: CursorFamily | undefined;
}) {
const label = () =>
props.style === "circle" ? "Circle" : CURSOR_FAMILIES[props.style].label;
props.style === "auto"
? "Default"
: props.style === "circle"
? "Circle"
: CURSOR_FAMILIES[props.style].label;

const tile = () => (
<div class="flex justify-center items-center w-full h-[60px] rounded-[10px] transition-shadow bg-ed-card-2 ring-1 ring-ed-line group-hover:ring-ed-line-strong group-data-checked:ring-2 group-data-checked:ring-ed-accent group-data-checked:ring-offset-2 group-data-checked:ring-offset-ed-card group-has-[input:focus-visible]:ring-2 group-has-[input:focus-visible]:ring-ed-accent">
<div
classList={{ "gap-3": props.style === "auto" }}
class="flex justify-center items-center w-full h-[60px] rounded-[10px] transition-shadow bg-ed-card-2 ring-1 ring-ed-line group-hover:ring-ed-line-strong group-data-checked:ring-2 group-data-checked:ring-ed-accent group-data-checked:ring-offset-2 group-data-checked:ring-offset-ed-card group-has-[input:focus-visible]:ring-2 group-has-[input:focus-visible]:ring-ed-accent"
>
<Show
when={props.style !== "circle" && props.style}
when={
props.style !== "circle" &&
(props.style === "auto"
? (props.recorded ?? hostCursorFamily())
: props.style)
}
fallback={<CircleCursor />}
>
{(family) => <CursorArrow svg={CURSOR_FAMILIES[family()].arrow} />}
</Show>
<Show when={props.style === "auto"}>
<span class="text-xs font-medium text-ed-text-1">Default</span>
<span class="text-[11px] text-ed-text-2">Recorded cursors</span>
</Show>
</div>
);

return (
<KRadioGroup.Item value={props.style} class="group min-w-0">
<KRadioGroup.Item
value={props.style}
class="group min-w-0"
classList={{ "col-span-4": props.style === "auto" }}
>
<KRadioGroup.ItemInput class="sr-only" />
<KRadioGroup.ItemLabel class="flex cursor-pointer flex-col items-center gap-1.5">
<Show when={props.recorded} fallback={tile()}>
<Tooltip content="Recorded with this cursor" childClass="w-full">
{tile()}
</Tooltip>
</Show>
<span class="max-w-full text-[11px] font-medium leading-none truncate transition-colors text-ed-text-2 group-hover:text-ed-text-1 group-data-checked:text-ed-text-1">
<Tooltip
content={cursorStyleDescription(props.style)}
childClass="w-full"
>
{tile()}
</Tooltip>
<span
classList={{ hidden: props.style === "auto" }}
class="max-w-full text-[11px] font-medium leading-none truncate transition-colors text-ed-text-2 group-hover:text-ed-text-1 group-data-checked:text-ed-text-1"
>
{label()}
</span>
</KRadioGroup.ItemLabel>
Expand All @@ -115,11 +142,7 @@ export function CursorStylePicker() {

const recorded = createMemo(() => recordedCursorFamily(meta()));

const selected = createMemo<CursorStyle>(() => {
const type = project.cursor.type;
if (type === "circle" || isExplicitCursorFamily(type)) return type;
return recorded() ?? hostCursorFamily();
});
const selected = createMemo(() => selectedCursorStyle(project.cursor.type));

return (
<Section name="Cursor style">
Expand All @@ -128,12 +151,13 @@ export function CursorStylePicker() {
value={selected()}
onChange={(value) => setProject("cursor", "type", value as CursorType)}
>
<For each={cursorStyleOrder()}>
{(style) => (
<CursorStyleCard style={style} recorded={recorded() === style} />
)}
<For each={cursorStyleOrder(ostype())}>
{(style) => <CursorStyleCard style={style} recorded={recorded()} />}
</For>
</KRadioGroup>
<p class="text-[11px] leading-relaxed text-ed-text-2">
{cursorStyleDescription(selected())}
</p>
</Section>
);
}
Expand Down
Loading
Loading