Fix shared element state across component instances
This commit is contained in:
@@ -510,8 +510,12 @@ where
|
|||||||
if state.highlight.is_some() {
|
if state.highlight.is_some() {
|
||||||
state.highlight = None;
|
state.highlight = None;
|
||||||
} else {
|
} else {
|
||||||
let query = state.query.read(cx).value().to_string();
|
if state.selected.is_some() {
|
||||||
state.refilter(&query);
|
state.refilter("");
|
||||||
|
} else {
|
||||||
|
let query = state.query.read(cx).value().to_string();
|
||||||
|
state.refilter(&query);
|
||||||
|
}
|
||||||
state.highlight = (!state.visible.is_empty()).then_some(0usize);
|
state.highlight = (!state.visible.is_empty()).then_some(0usize);
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
//! The command palette: a modal query field over the application's actions.
|
//! The command palette: a modal query field over the application's actions.
|
||||||
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Bounds, Context, ElementId, Entity, InteractiveElement as _, IntoElement,
|
AnyElement, App, Context, ElementId, Entity, InteractiveElement as _, IntoElement, KeyBinding,
|
||||||
KeyBinding, MouseButton, MouseDownEvent, ParentElement as _, Pixels, Render, Role,
|
MouseButton, ParentElement as _, Render, Role, ScrollHandle, SharedString,
|
||||||
ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription, Window,
|
StatefulInteractiveElement as _, Styled as _, Subscription, Window, actions, canvas, deferred,
|
||||||
actions, canvas, div, px, relative,
|
div, px, relative,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::{IconRole, icon};
|
||||||
use gpui_native_theme::{ActiveTheme as _, Size};
|
use gpui_native_theme::{ActiveTheme as _, Size};
|
||||||
@@ -322,6 +321,7 @@ impl Render for CommandPaletteState {
|
|||||||
|
|
||||||
let id = self.id.clone();
|
let id = self.id.clone();
|
||||||
let state = cx.entity();
|
let state = cx.entity();
|
||||||
|
let query_focus = self.query.read(cx).focus_handle();
|
||||||
|
|
||||||
let rows: Vec<AnyElement> = self
|
let rows: Vec<AnyElement> = self
|
||||||
.matches
|
.matches
|
||||||
@@ -406,9 +406,6 @@ impl Render for CommandPaletteState {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
};
|
};
|
||||||
|
|
||||||
let panel_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None));
|
|
||||||
let query_focus = self.query.read(cx).focus_handle();
|
|
||||||
|
|
||||||
let panel = v_flex()
|
let panel = v_flex()
|
||||||
.id((id.clone(), "panel"))
|
.id((id.clone(), "panel"))
|
||||||
.debug_selector({
|
.debug_selector({
|
||||||
@@ -460,20 +457,14 @@ impl Render for CommandPaletteState {
|
|||||||
)
|
)
|
||||||
.child(rows)
|
.child(rows)
|
||||||
.child(
|
.child(
|
||||||
canvas(
|
canvas(|_, _, _| {}, {
|
||||||
{
|
let focus = query_focus.clone();
|
||||||
let cell = panel_bounds.clone();
|
move |_, _, window, cx| {
|
||||||
move |bounds, _window, _cx| *cell.borrow_mut() = Some(bounds)
|
if !focus.contains_focused(window, cx) {
|
||||||
},
|
window.focus(&focus, cx);
|
||||||
{
|
|
||||||
let focus = query_focus.clone();
|
|
||||||
move |_, _, window, cx| {
|
|
||||||
if !focus.contains_focused(window, cx) {
|
|
||||||
window.focus(&focus, cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
})
|
||||||
.absolute()
|
.absolute()
|
||||||
.inset_0(),
|
.inset_0(),
|
||||||
);
|
);
|
||||||
@@ -485,28 +476,23 @@ impl Render for CommandPaletteState {
|
|||||||
.occlude()
|
.occlude()
|
||||||
.on_mouse_down(MouseButton::Left, {
|
.on_mouse_down(MouseButton::Left, {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
let panel_bounds = panel_bounds.clone();
|
move |_, _window, cx| {
|
||||||
move |event: &MouseDownEvent, _window, cx| {
|
|
||||||
let on_panel = panel_bounds
|
|
||||||
.borrow()
|
|
||||||
.is_some_and(|bounds| bounds.contains(&event.position));
|
|
||||||
if on_panel {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.update(cx, |state, cx| state.close(cx));
|
state.update(cx, |state, cx| state.close(cx));
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
div()
|
deferred(
|
||||||
.absolute()
|
div()
|
||||||
.inset_0()
|
.absolute()
|
||||||
.flex()
|
.inset_0()
|
||||||
.justify_center()
|
.flex()
|
||||||
.items_start()
|
.justify_center()
|
||||||
.pt_10()
|
.items_start()
|
||||||
.child(scrim)
|
.pt_10()
|
||||||
.child(panel)
|
.child(scrim)
|
||||||
.into_any_element()
|
.child(panel),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,7 +207,8 @@ impl RenderOnce for DropdownMenu {
|
|||||||
let a11y_label = self.a11y_label.or_else(|| self.label.clone());
|
let a11y_label = self.a11y_label.or_else(|| self.label.clone());
|
||||||
let focus = focus_handle(&self.id, window, cx).tab_stop(self.tab_stop);
|
let focus = focus_handle(&self.id, window, cx).tab_stop(self.tab_stop);
|
||||||
// How the menu was last opened, read by the frame that mounts the panel.
|
// How the menu was last opened, read by the frame that mounts the panel.
|
||||||
let keyboard_open = window.use_keyed_state("dropdown-open-origin", cx, |_, _| false);
|
let keyboard_open =
|
||||||
|
window.use_keyed_state((self.id.clone(), "dropdown-open-origin"), cx, |_, _| false);
|
||||||
|
|
||||||
let on_open: ChangeHandler = {
|
let on_open: ChangeHandler = {
|
||||||
let focus = focus.clone();
|
let focus = focus.clone();
|
||||||
|
|||||||
@@ -1799,7 +1799,9 @@ impl EntityInputHandler for InputState {
|
|||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
let layout = self.layout.borrow();
|
let layout = self.layout.borrow();
|
||||||
let layout = layout.as_ref()?;
|
let layout = layout.as_ref()?;
|
||||||
Some(real_index(&layout.mask_map, layout.index_at(window_point)).min(self.text.len()))
|
let index =
|
||||||
|
real_index(&layout.mask_map, layout.index_at(window_point)).min(self.text.len());
|
||||||
|
Some(utf8_range_to_utf16(&self.text, index..index).start)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_length_utf16(
|
fn text_length_utf16(
|
||||||
@@ -2516,7 +2518,9 @@ fn utf16_to_utf8_offset(text: &str, utf16_offset: usize) -> usize {
|
|||||||
|
|
||||||
/// Converts a UTF-8 byte range to a UTF-16 range.
|
/// Converts a UTF-8 byte range to a UTF-16 range.
|
||||||
fn utf8_range_to_utf16(text: &str, range: Range<usize>) -> Range<usize> {
|
fn utf8_range_to_utf16(text: &str, range: Range<usize>) -> Range<usize> {
|
||||||
utf16_to_utf8_offset(text, range.start)..utf16_to_utf8_offset(text, range.end)
|
let start = text[..range.start].encode_utf16().count();
|
||||||
|
let end = start + text[range.start..range.end].encode_utf16().count();
|
||||||
|
start..end
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a UTF-16 range to a UTF-8 byte range.
|
/// Converts a UTF-16 range to a UTF-8 byte range.
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ pub(crate) fn anchored_menu_panel(
|
|||||||
chrome: &MenuChrome,
|
chrome: &MenuChrome,
|
||||||
enter_duration: std::time::Duration,
|
enter_duration: std::time::Duration,
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
let was_open = window.use_keyed_state("menu-was-open", cx, |_, _| false);
|
let was_open = window.use_keyed_state((id.clone(), "menu-was-open"), cx, |_, _| false);
|
||||||
|
|
||||||
if !open {
|
if !open {
|
||||||
was_open.update(cx, |was_open, _| *was_open = false);
|
was_open.update(cx, |was_open, _| *was_open = false);
|
||||||
@@ -392,8 +392,9 @@ pub(crate) fn anchored_menu_panel(
|
|||||||
let on_change = on_change.clone();
|
let on_change = on_change.clone();
|
||||||
// Keyboard focus rides the panel across frames
|
// Keyboard focus rides the panel across frames
|
||||||
let focus = gpui_native_utils::focus_handle(&(id.clone(), "panel").into(), window, cx);
|
let focus = gpui_native_utils::focus_handle(&(id.clone(), "panel").into(), window, cx);
|
||||||
let scroll = window.use_keyed_state("menu-scroll", cx, |_, _| ScrollHandle::new());
|
let scroll =
|
||||||
let selection = window.use_keyed_state("menu-selection", cx, |_, _| None);
|
window.use_keyed_state((id.clone(), "menu-scroll"), cx, |_, _| ScrollHandle::new());
|
||||||
|
let selection = window.use_keyed_state((id.clone(), "menu-selection"), cx, |_, _| None);
|
||||||
|
|
||||||
let selected = if *was_open.read(cx) {
|
let selected = if *was_open.read(cx) {
|
||||||
*selection.read(cx)
|
*selection.read(cx)
|
||||||
|
|||||||
@@ -128,33 +128,35 @@ pub struct Scrollbar {
|
|||||||
axis: Axis,
|
axis: Axis,
|
||||||
handle: Rc<dyn ScrollbarHandle>,
|
handle: Rc<dyn ScrollbarHandle>,
|
||||||
visibility: ScrollbarVisibility,
|
visibility: ScrollbarVisibility,
|
||||||
key: ElementId,
|
id: ElementId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scrollbar {
|
impl Scrollbar {
|
||||||
/// A vertical scrollbar driven by `handle`.
|
/// A vertical scrollbar named by `id`, driven by `handle`.
|
||||||
pub fn vertical<H>(handle: H) -> Self
|
pub fn vertical<I, H>(id: I, handle: H) -> Self
|
||||||
where
|
where
|
||||||
|
I: Into<ElementId>,
|
||||||
H: ScrollbarHandle,
|
H: ScrollbarHandle,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
axis: Axis::Vertical,
|
axis: Axis::Vertical,
|
||||||
handle: Rc::new(handle),
|
handle: Rc::new(handle),
|
||||||
visibility: ScrollbarVisibility::default(),
|
visibility: ScrollbarVisibility::default(),
|
||||||
key: ElementId::Name("scrollbar".into()),
|
id: id.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A horizontal scrollbar driven by `handle`.
|
/// A horizontal scrollbar named by `id`, driven by `handle`.
|
||||||
pub fn horizontal<H>(handle: H) -> Self
|
pub fn horizontal<I, H>(id: I, handle: H) -> Self
|
||||||
where
|
where
|
||||||
|
I: Into<ElementId>,
|
||||||
H: ScrollbarHandle,
|
H: ScrollbarHandle,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
axis: Axis::Horizontal,
|
axis: Axis::Horizontal,
|
||||||
handle: Rc::new(handle),
|
handle: Rc::new(handle),
|
||||||
visibility: ScrollbarVisibility::default(),
|
visibility: ScrollbarVisibility::default(),
|
||||||
key: ElementId::Name("scrollbar".into()),
|
id: id.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,12 +165,6 @@ impl Scrollbar {
|
|||||||
self.visibility = visibility;
|
self.visibility = visibility;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Names this scrollbar's state.
|
|
||||||
pub fn key(mut self, key: impl Into<ElementId>) -> Self {
|
|
||||||
self.key = key.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Scrollbar {
|
impl RenderOnce for Scrollbar {
|
||||||
@@ -177,8 +173,8 @@ impl RenderOnce for Scrollbar {
|
|||||||
let handle = self.handle;
|
let handle = self.handle;
|
||||||
let axis = self.axis;
|
let axis = self.axis;
|
||||||
let visibility = self.visibility;
|
let visibility = self.visibility;
|
||||||
let key = self.key;
|
let id = self.id;
|
||||||
move |_, _, window, cx| paint_scrollbar(&handle, axis, visibility, &key, window, cx)
|
move |_, _, window, cx| paint_scrollbar(&handle, axis, visibility, &id, window, cx)
|
||||||
})
|
})
|
||||||
.absolute()
|
.absolute()
|
||||||
.inset_0()
|
.inset_0()
|
||||||
@@ -192,7 +188,7 @@ fn paint_scrollbar(
|
|||||||
handle: &Rc<dyn ScrollbarHandle>,
|
handle: &Rc<dyn ScrollbarHandle>,
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
visibility: ScrollbarVisibility,
|
visibility: ScrollbarVisibility,
|
||||||
key: &ElementId,
|
id: &ElementId,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
@@ -217,12 +213,10 @@ fn paint_scrollbar(
|
|||||||
ScrollbarVisibility::Always => false,
|
ScrollbarVisibility::Always => false,
|
||||||
ScrollbarVisibility::Autohide => true,
|
ScrollbarVisibility::Autohide => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let autohide_state =
|
let autohide_state =
|
||||||
autohide.then(|| Autohide::new(scrollbar_state_key("autohide", key), window, cx));
|
autohide.then(|| Autohide::new(scrollbar_state_key("autohide", id), window, cx));
|
||||||
|
|
||||||
let track_length = viewport_along - inset * 2.0;
|
let track_length = viewport_along - inset * 2.0;
|
||||||
|
|
||||||
let (thumb_length, thumb_travel) = thumb_geometry(
|
let (thumb_length, thumb_travel) = thumb_geometry(
|
||||||
viewport_along,
|
viewport_along,
|
||||||
viewport_along + max_offset,
|
viewport_along + max_offset,
|
||||||
@@ -267,9 +261,7 @@ fn paint_scrollbar(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let drag = window.use_keyed_state(scrollbar_state_key("thumb-drag", key), cx, |_, _| {
|
let drag = window.use_keyed_state(scrollbar_state_key("thumb-drag", id), cx, |_, _| Drag::None);
|
||||||
Drag::None
|
|
||||||
});
|
|
||||||
let dragging = drag.read(cx).is_some();
|
let dragging = drag.read(cx).is_some();
|
||||||
|
|
||||||
let alpha = match &autohide_state {
|
let alpha = match &autohide_state {
|
||||||
|
|||||||
@@ -135,16 +135,12 @@ impl RenderOnce for Slider {
|
|||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
// The track bounds, recorded by the overlay canvas each frame,
|
|
||||||
// turn pointer positions into values inside the listeners.
|
|
||||||
let bounds_cell = Rc::new(Cell::new(None::<Bounds<Pixels>>));
|
let bounds_cell = Rc::new(Cell::new(None::<Bounds<Pixels>>));
|
||||||
// Dragging crosses frames, so it lives in keyed element state,
|
|
||||||
// a per-render flag would reset when the owner's update repaints.
|
|
||||||
let dragging = window.use_keyed_state("slider-dragging", cx, |_, _| false);
|
|
||||||
// A held handle gives the arrow-key listener something to check focus against,
|
|
||||||
// and the click-to-focus behavior a target.
|
|
||||||
let focus_handle = gpui_native_utils::focus_handle(&self.id, window, cx);
|
let focus_handle = gpui_native_utils::focus_handle(&self.id, window, cx);
|
||||||
|
|
||||||
|
let dragging =
|
||||||
|
window.use_keyed_state((self.id.clone(), "slider-dragging"), cx, |_, _| false);
|
||||||
|
|
||||||
let set_value: ValueSetter = {
|
let set_value: ValueSetter = {
|
||||||
let bounds_cell = bounds_cell.clone();
|
let bounds_cell = bounds_cell.clone();
|
||||||
let on_change = self.on_change.clone();
|
let on_change = self.on_change.clone();
|
||||||
|
|||||||
@@ -322,3 +322,136 @@ fn disabled_trigger_ignores_clicks() {
|
|||||||
assert!(changes.borrow().is_empty());
|
assert!(changes.borrow().is_empty());
|
||||||
assert!(visual.debug_bounds("menu-panel").is_none());
|
assert!(visual.debug_bounds("menu-panel").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PairHarness {
|
||||||
|
open_a: bool,
|
||||||
|
open_b: bool,
|
||||||
|
changes_a: Changes,
|
||||||
|
changes_b: Changes,
|
||||||
|
activated_a: Activated,
|
||||||
|
activated_b: Activated,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for PairHarness {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let (changes_a, changes_b) = (self.changes_a.clone(), self.changes_b.clone());
|
||||||
|
let (activated_a, activated_b) = (self.activated_a.clone(), self.activated_b.clone());
|
||||||
|
|
||||||
|
let menu_a = DropdownMenu::new("pair-a")
|
||||||
|
.label("Alpha")
|
||||||
|
.variant(ButtonVariant::Secondary)
|
||||||
|
.open(self.open_a)
|
||||||
|
.on_open_change({
|
||||||
|
let view = cx.entity();
|
||||||
|
let changes = changes_a.clone();
|
||||||
|
move |next, _, cx| {
|
||||||
|
changes.borrow_mut().push(next);
|
||||||
|
view.update(cx, |harness, cx| {
|
||||||
|
harness.open_a = next;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.items(move |_, _| {
|
||||||
|
vec![
|
||||||
|
MenuItem::new("png").handler({
|
||||||
|
let activated = activated_a.clone();
|
||||||
|
move |_, _| activated.borrow_mut().push("png")
|
||||||
|
}),
|
||||||
|
MenuItem::new("pdf").handler({
|
||||||
|
let activated = activated_a.clone();
|
||||||
|
move |_, _| activated.borrow_mut().push("pdf")
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
let menu_b = DropdownMenu::new("pair-b")
|
||||||
|
.label("Beta")
|
||||||
|
.variant(ButtonVariant::Secondary)
|
||||||
|
.open(self.open_b)
|
||||||
|
.on_open_change({
|
||||||
|
let view = cx.entity();
|
||||||
|
let changes = changes_b.clone();
|
||||||
|
move |next, _, cx| {
|
||||||
|
changes.borrow_mut().push(next);
|
||||||
|
view.update(cx, |harness, cx| {
|
||||||
|
harness.open_b = next;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.items(move |_, _| {
|
||||||
|
vec![
|
||||||
|
MenuItem::new("zip").handler({
|
||||||
|
let activated = activated_b.clone();
|
||||||
|
move |_, _| activated.borrow_mut().push("zip")
|
||||||
|
}),
|
||||||
|
MenuItem::new("7z").handler({
|
||||||
|
let activated = activated_b.clone();
|
||||||
|
move |_, _| activated.borrow_mut().push("7z")
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
div().size_full().flex().gap_2().child(menu_a).child(menu_b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pair_window() -> (
|
||||||
|
VisualTestContext,
|
||||||
|
AnyWindowHandle,
|
||||||
|
Changes,
|
||||||
|
Changes,
|
||||||
|
Activated,
|
||||||
|
Activated,
|
||||||
|
) {
|
||||||
|
let mut cx = TestAppContext::single();
|
||||||
|
cx.update(Theme::init);
|
||||||
|
let changes_a = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let changes_b = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let activated_a = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let activated_b = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let (changes_a_for_window, changes_b_for_window) = (changes_a.clone(), changes_b.clone());
|
||||||
|
let (activated_a_for_window, activated_b_for_window) =
|
||||||
|
(activated_a.clone(), activated_b.clone());
|
||||||
|
let window = cx.add_window(move |_, _| PairHarness {
|
||||||
|
open_a: false,
|
||||||
|
open_b: false,
|
||||||
|
changes_a: changes_a_for_window,
|
||||||
|
changes_b: changes_b_for_window,
|
||||||
|
activated_a: activated_a_for_window,
|
||||||
|
activated_b: activated_b_for_window,
|
||||||
|
});
|
||||||
|
let window = window.into();
|
||||||
|
cx.update_window(window, |_, window, cx| {
|
||||||
|
window.draw(cx).clear(cx);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
(
|
||||||
|
VisualTestContext::from_window(window, &cx),
|
||||||
|
window,
|
||||||
|
changes_a,
|
||||||
|
changes_b,
|
||||||
|
activated_a,
|
||||||
|
activated_b,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// With a closed sibling dropdown on screen, a pointer-opened menu must still
|
||||||
|
/// arm rows with the keyboard: the menu's open/selection state is per
|
||||||
|
/// instance, not shared keyed state.
|
||||||
|
#[test]
|
||||||
|
fn sibling_dropdowns_keep_independent_menu_state() {
|
||||||
|
let (mut visual, window, changes_a, changes_b, activated_a, activated_b) = pair_window();
|
||||||
|
|
||||||
|
// Menu A sits at the window's top-left; menu B stays closed beside it.
|
||||||
|
click(&mut visual, point(px(30.), px(14.)));
|
||||||
|
assert_eq!(*changes_a.borrow(), vec![true]);
|
||||||
|
draw(&mut visual, window);
|
||||||
|
|
||||||
|
visual.simulate_keystrokes("down enter");
|
||||||
|
assert_eq!(*activated_a.borrow(), vec!["png"]);
|
||||||
|
assert_eq!(*changes_a.borrow(), vec![true, false]);
|
||||||
|
assert!(changes_b.borrow().is_empty(), "B must stay closed");
|
||||||
|
assert!(activated_b.borrow().is_empty());
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ impl Render for UniformShell {
|
|||||||
.h_full()
|
.h_full()
|
||||||
.w_full(),
|
.w_full(),
|
||||||
)
|
)
|
||||||
.child(Scrollbar::vertical(self.handle.clone())),
|
.child(Scrollbar::vertical("rows", self.handle.clone())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ impl Render for ListShell {
|
|||||||
.h_full()
|
.h_full()
|
||||||
.w_full(),
|
.w_full(),
|
||||||
)
|
)
|
||||||
.child(Scrollbar::vertical(self.state.clone())),
|
.child(Scrollbar::vertical("rows", self.state.clone())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,3 +271,99 @@ fn dragging_the_thumb_drives_a_list_state_and_reports_the_drag() {
|
|||||||
"the drag ends with the mouse"
|
"the drag ends with the mouse"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TwoShell {
|
||||||
|
left: UniformListScrollHandle,
|
||||||
|
right: UniformListScrollHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TwoShell {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.relative()
|
||||||
|
.h(px(VIEWPORT_HEIGHT))
|
||||||
|
.w(px(300.))
|
||||||
|
.child(
|
||||||
|
uniform_list("left-rows", ROWS, |range, _, _| {
|
||||||
|
range
|
||||||
|
.map(|ix| div().h(px(ROW_HEIGHT)).child(format!("row {ix}")))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.track_scroll(&self.left)
|
||||||
|
.h_full()
|
||||||
|
.w_full(),
|
||||||
|
)
|
||||||
|
.child(Scrollbar::vertical("left", self.left.clone())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.relative()
|
||||||
|
.h(px(VIEWPORT_HEIGHT))
|
||||||
|
.w(px(300.))
|
||||||
|
.child(
|
||||||
|
uniform_list("right-rows", ROWS, |range, _, _| {
|
||||||
|
range
|
||||||
|
.map(|ix| div().h(px(ROW_HEIGHT)).child(format!("row {ix}")))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.track_scroll(&self.right)
|
||||||
|
.h_full()
|
||||||
|
.w_full(),
|
||||||
|
)
|
||||||
|
.child(Scrollbar::vertical("right", self.right.clone())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn two_shell() -> (
|
||||||
|
TestAppContext,
|
||||||
|
VisualTestContext,
|
||||||
|
UniformListScrollHandle,
|
||||||
|
UniformListScrollHandle,
|
||||||
|
) {
|
||||||
|
let mut cx = TestAppContext::single();
|
||||||
|
cx.update(init);
|
||||||
|
let (left, right) = (
|
||||||
|
UniformListScrollHandle::new(),
|
||||||
|
UniformListScrollHandle::new(),
|
||||||
|
);
|
||||||
|
let (left_for_view, right_for_view) = (left.clone(), right.clone());
|
||||||
|
let window = cx.add_window(move |_, _| TwoShell {
|
||||||
|
left: left_for_view,
|
||||||
|
right: right_for_view,
|
||||||
|
});
|
||||||
|
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
||||||
|
visual.update(|window, cx| {
|
||||||
|
window.draw(cx).clear(cx);
|
||||||
|
});
|
||||||
|
(cx, visual, left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dragging one scrollbar's thumb must never move a sibling scrollbar: the
|
||||||
|
/// drag state is keyed per scrollbar id, not shared across instances.
|
||||||
|
#[test]
|
||||||
|
fn sibling_scrollbars_drag_independently() {
|
||||||
|
let (cx, mut visual, left, right) = two_shell();
|
||||||
|
let metrics = Metrics::measure(&cx, &left);
|
||||||
|
assert!(metrics.max_offset > px(0.));
|
||||||
|
|
||||||
|
press(&mut visual, metrics.thumb_center());
|
||||||
|
drag_to(&mut visual, metrics.full_drag_target());
|
||||||
|
release(&mut visual, metrics.full_drag_target());
|
||||||
|
|
||||||
|
let offset = left.offset().y;
|
||||||
|
assert!(
|
||||||
|
(offset.abs() - metrics.max_offset).abs() < px(0.5),
|
||||||
|
"the left drag reaches the end, got {offset}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
right.offset().y,
|
||||||
|
px(0.),
|
||||||
|
"the left drag leaked into the right scrollbar"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -153,3 +153,132 @@ fn arrow_keys_step_the_value_while_focused() {
|
|||||||
|
|
||||||
assert_eq!(*changes.borrow(), vec![0.0, 10.0, 20.0, 10.0]);
|
assert_eq!(*changes.borrow(), vec![0.0, 10.0, 20.0, 10.0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TwoSliders {
|
||||||
|
changes_a: Rc<RefCell<Vec<f32>>>,
|
||||||
|
changes_b: Rc<RefCell<Vec<f32>>>,
|
||||||
|
value_a: f32,
|
||||||
|
value_b: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TwoSliders {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let (changes_a, changes_b) = (self.changes_a.clone(), self.changes_b.clone());
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.child(
|
||||||
|
div().absolute().top_0().left_0().w(px(200.)).child(
|
||||||
|
Slider::new("slider-a", self.value_a)
|
||||||
|
.min(0.0)
|
||||||
|
.max(100.0)
|
||||||
|
.step(10.0)
|
||||||
|
.on_change({
|
||||||
|
let view = cx.entity();
|
||||||
|
let changes_a = changes_a.clone();
|
||||||
|
move |next, _, cx| {
|
||||||
|
changes_a.borrow_mut().push(next);
|
||||||
|
view.update(cx, |harness, cx| {
|
||||||
|
harness.value_a = next;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div().absolute().top(px(40.)).left_0().w(px(200.)).child(
|
||||||
|
Slider::new("slider-b", self.value_b)
|
||||||
|
.min(0.0)
|
||||||
|
.max(100.0)
|
||||||
|
.step(10.0)
|
||||||
|
.on_change({
|
||||||
|
let view = cx.entity();
|
||||||
|
move |next, _, cx| {
|
||||||
|
changes_b.borrow_mut().push(next);
|
||||||
|
view.update(cx, |harness, cx| {
|
||||||
|
harness.value_b = next;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn two_slider_window() -> (
|
||||||
|
VisualTestContext,
|
||||||
|
Rc<RefCell<Vec<f32>>>,
|
||||||
|
Rc<RefCell<Vec<f32>>>,
|
||||||
|
) {
|
||||||
|
let mut cx = TestAppContext::single();
|
||||||
|
cx.update(Theme::init);
|
||||||
|
let changes_a = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let changes_b = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let (changes_a_for_window, changes_b_for_window) = (changes_a.clone(), changes_b.clone());
|
||||||
|
let window = cx.add_window(move |_, _| TwoSliders {
|
||||||
|
changes_a: changes_a_for_window,
|
||||||
|
changes_b: changes_b_for_window,
|
||||||
|
value_a: 0.0,
|
||||||
|
value_b: 0.0,
|
||||||
|
});
|
||||||
|
let window = window.into();
|
||||||
|
cx.update_window(window, |_, window, cx| {
|
||||||
|
window.draw(cx).clear(cx);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
(
|
||||||
|
VisualTestContext::from_window(window, &cx),
|
||||||
|
changes_a,
|
||||||
|
changes_b,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dragging one slider must never drive a sibling slider: the drag flag is
|
||||||
|
/// per instance, not shared keyed state.
|
||||||
|
#[test]
|
||||||
|
fn two_sliders_drag_independently() {
|
||||||
|
let (mut cx, changes_a, changes_b) = two_slider_window();
|
||||||
|
|
||||||
|
// Slider A occupies y 0..16, slider B starts at y 40.
|
||||||
|
cx.simulate_mouse_down(
|
||||||
|
point(px(100.), px(10.)),
|
||||||
|
MouseButton::Left,
|
||||||
|
Modifiers::default(),
|
||||||
|
);
|
||||||
|
cx.simulate_mouse_move(
|
||||||
|
point(px(120.), px(10.)),
|
||||||
|
MouseButton::Left,
|
||||||
|
Modifiers::default(),
|
||||||
|
);
|
||||||
|
cx.simulate_mouse_up(
|
||||||
|
point(px(120.), px(10.)),
|
||||||
|
MouseButton::Left,
|
||||||
|
Modifiers::default(),
|
||||||
|
);
|
||||||
|
assert_eq!(*changes_a.borrow(), vec![50.0, 60.0]);
|
||||||
|
assert!(changes_b.borrow().is_empty(), "A's drag leaked into B");
|
||||||
|
|
||||||
|
cx.simulate_mouse_down(
|
||||||
|
point(px(100.), px(50.)),
|
||||||
|
MouseButton::Left,
|
||||||
|
Modifiers::default(),
|
||||||
|
);
|
||||||
|
cx.simulate_mouse_move(
|
||||||
|
point(px(40.), px(50.)),
|
||||||
|
MouseButton::Left,
|
||||||
|
Modifiers::default(),
|
||||||
|
);
|
||||||
|
cx.simulate_mouse_up(
|
||||||
|
point(px(40.), px(50.)),
|
||||||
|
MouseButton::Left,
|
||||||
|
Modifiers::default(),
|
||||||
|
);
|
||||||
|
assert_eq!(*changes_b.borrow(), vec![50.0, 20.0]);
|
||||||
|
assert_eq!(
|
||||||
|
*changes_a.borrow(),
|
||||||
|
vec![50.0, 60.0],
|
||||||
|
"B's drag leaked into A"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ pub enum TabsVariant {
|
|||||||
|
|
||||||
/// One tab of a [`Tabs`] strip.
|
/// One tab of a [`Tabs`] strip.
|
||||||
pub struct Tab {
|
pub struct Tab {
|
||||||
|
/// The stable identity for element state; must survive reordering.
|
||||||
|
key: ElementId,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
icon: Option<IconRole>,
|
icon: Option<IconRole>,
|
||||||
lead: Option<AnyElement>,
|
lead: Option<AnyElement>,
|
||||||
@@ -42,12 +44,14 @@ pub struct Tab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Tab {
|
impl Tab {
|
||||||
/// Creates a tab with this label.
|
/// Creates a tab shown as `label`, identified by `key`.
|
||||||
pub fn new<L>(label: L) -> Self
|
pub fn new<K, L>(key: K, label: L) -> Self
|
||||||
where
|
where
|
||||||
|
K: Into<ElementId>,
|
||||||
L: Into<SharedString>,
|
L: Into<SharedString>,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
|
key: key.into(),
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
icon: None,
|
icon: None,
|
||||||
lead: None,
|
lead: None,
|
||||||
@@ -301,7 +305,7 @@ fn render_tab(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut strip_tab = div()
|
let mut strip_tab = div()
|
||||||
.id(("tab", index))
|
.id((tab.key.clone(), "tab"))
|
||||||
.debug_selector({
|
.debug_selector({
|
||||||
let id = format!("{tabs_id}-tab-{}", tab.label);
|
let id = format!("{tabs_id}-tab-{}", tab.label);
|
||||||
move || id
|
move || id
|
||||||
@@ -364,7 +368,7 @@ fn render_tab(
|
|||||||
|
|
||||||
if tab.busy && !tab.disabled {
|
if tab.busy && !tab.disabled {
|
||||||
strip_tab = strip_tab.child(
|
strip_tab = strip_tab.child(
|
||||||
Spinner::new(("tab-busy", index))
|
Spinner::new((tab.key.clone(), "tab-busy"))
|
||||||
.small()
|
.small()
|
||||||
.accessibility_label("Loading"),
|
.accessibility_label("Loading"),
|
||||||
);
|
);
|
||||||
@@ -401,7 +405,7 @@ fn render_tab_close(
|
|||||||
index: usize,
|
index: usize,
|
||||||
on_close: &Option<CloseHandler>,
|
on_close: &Option<CloseHandler>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
Button::new(("tab-close", index))
|
Button::new((tab.key.clone(), "tab-close"))
|
||||||
.icon(IconRole::Close)
|
.icon(IconRole::Close)
|
||||||
.variant(ButtonVariant::Ghost)
|
.variant(ButtonVariant::Ghost)
|
||||||
.small()
|
.small()
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ impl Render for Harness {
|
|||||||
})
|
})
|
||||||
.tabs(|_, _| {
|
.tabs(|_, _| {
|
||||||
vec![
|
vec![
|
||||||
Tab::new("One"),
|
Tab::new("one", "One"),
|
||||||
Tab::new("Two"),
|
Tab::new("two", "Two"),
|
||||||
Tab::new("Off").disabled(true),
|
Tab::new("off", "Off").disabled(true),
|
||||||
Tab::new("End"),
|
Tab::new("end", "End"),
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use gpui::{
|
|||||||
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility};
|
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility};
|
||||||
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Ramps};
|
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Ramps};
|
||||||
|
|
||||||
use crate::highlight::{HighlightedCode, Language, Role, highlight_with_tree};
|
use crate::highlight::{HighlightedCode, Language, Role, highlight};
|
||||||
use crate::utils::{gutter_cell, run_style};
|
use crate::utils::{gutter_cell, run_style};
|
||||||
|
|
||||||
actions!(code_view, [Copy]);
|
actions!(code_view, [Copy]);
|
||||||
@@ -53,8 +53,6 @@ pub struct CodeViewState {
|
|||||||
source: SharedString,
|
source: SharedString,
|
||||||
language: Language,
|
language: Language,
|
||||||
code: HighlightedCode,
|
code: HighlightedCode,
|
||||||
/// The parse tree of `source`, so updates reparse incrementally instead of from scratch.
|
|
||||||
tree: Option<tree_sitter::Tree>,
|
|
||||||
handle: UniformListScrollHandle,
|
handle: UniformListScrollHandle,
|
||||||
/// The hovered line, driving the active-line wash and number emphasis.
|
/// The hovered line, driving the active-line wash and number emphasis.
|
||||||
active_line: Option<usize>,
|
active_line: Option<usize>,
|
||||||
@@ -74,14 +72,13 @@ impl CodeViewState {
|
|||||||
S: Into<SharedString>,
|
S: Into<SharedString>,
|
||||||
{
|
{
|
||||||
let source = source.into();
|
let source = source.into();
|
||||||
let (code, tree) = highlight_with_tree(&source, language, None);
|
let code = highlight(&source, language);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
source,
|
source,
|
||||||
language,
|
language,
|
||||||
code,
|
code,
|
||||||
tree,
|
|
||||||
handle: UniformListScrollHandle::new(),
|
handle: UniformListScrollHandle::new(),
|
||||||
active_line: None,
|
active_line: None,
|
||||||
selection: None,
|
selection: None,
|
||||||
@@ -98,18 +95,9 @@ impl CodeViewState {
|
|||||||
{
|
{
|
||||||
let source = source.into();
|
let source = source.into();
|
||||||
|
|
||||||
// The old tree is only reusable when the grammar stays the same.
|
self.code = highlight(&source, language);
|
||||||
let old_tree = if language == self.language {
|
|
||||||
self.tree.as_ref()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let (code, tree) = highlight_with_tree(&source, language, old_tree);
|
|
||||||
self.source = source;
|
self.source = source;
|
||||||
self.language = language;
|
self.language = language;
|
||||||
self.code = code;
|
|
||||||
self.tree = tree;
|
|
||||||
self.selection = None;
|
self.selection = None;
|
||||||
self.handle.scroll_to_item(0, ScrollStrategy::Top);
|
self.handle.scroll_to_item(0, ScrollStrategy::Top);
|
||||||
|
|
||||||
@@ -134,9 +122,7 @@ impl CodeViewState {
|
|||||||
lines.splice(line_ix..line_ix + 1, replacement);
|
lines.splice(line_ix..line_ix + 1, replacement);
|
||||||
self.source = (lines.join("\n") + if trailing_newline { "\n" } else { "" }).into();
|
self.source = (lines.join("\n") + if trailing_newline { "\n" } else { "" }).into();
|
||||||
|
|
||||||
let (code, tree) = highlight_with_tree(&self.source, self.language, self.tree.as_ref());
|
self.code = highlight(&self.source, self.language);
|
||||||
self.code = code;
|
|
||||||
self.tree = tree;
|
|
||||||
self.selection = None;
|
self.selection = None;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -348,7 +334,7 @@ impl Render for CodeViewState {
|
|||||||
code.position_at(
|
code.position_at(
|
||||||
*bounds,
|
*bounds,
|
||||||
event.position,
|
event.position,
|
||||||
Pixels::ZERO,
|
ScrollbarHandle::offset(&code.handle).y,
|
||||||
line_height,
|
line_height,
|
||||||
char_width,
|
char_width,
|
||||||
text_x,
|
text_x,
|
||||||
@@ -453,7 +439,8 @@ impl Render for CodeViewState {
|
|||||||
)
|
)
|
||||||
.child(rows)
|
.child(rows)
|
||||||
.child(
|
.child(
|
||||||
Scrollbar::vertical(self.handle.clone()).visibility(ScrollbarVisibility::Platform),
|
Scrollbar::vertical((self.id.clone(), "v-scrollbar"), self.handle.clone())
|
||||||
|
.visibility(ScrollbarVisibility::Platform),
|
||||||
)
|
)
|
||||||
.when_some(self.a11y_label.clone(), |view, label| {
|
.when_some(self.a11y_label.clone(), |view, label| {
|
||||||
view.aria_label(label)
|
view.aria_label(label)
|
||||||
|
|||||||
@@ -363,13 +363,11 @@ impl Render for DiffViewState {
|
|||||||
})
|
})
|
||||||
.child(rows)
|
.child(rows)
|
||||||
.child(
|
.child(
|
||||||
Scrollbar::vertical(self.handle.clone())
|
Scrollbar::vertical((self.id.clone(), "v-scrollbar"), self.handle.clone())
|
||||||
.key("v-scrollbar")
|
|
||||||
.visibility(ScrollbarVisibility::Platform),
|
.visibility(ScrollbarVisibility::Platform),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Scrollbar::horizontal(self.handle.clone())
|
Scrollbar::horizontal((self.id.clone(), "h-scrollbar"), self.handle.clone())
|
||||||
.key("h-scrollbar")
|
|
||||||
.visibility(ScrollbarVisibility::Platform),
|
.visibility(ScrollbarVisibility::Platform),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,17 +67,8 @@ pub struct HighlightedCode {
|
|||||||
|
|
||||||
/// Highlights `source` with `language`'s tree-sitter grammar.
|
/// Highlights `source` with `language`'s tree-sitter grammar.
|
||||||
pub fn highlight(source: &str, language: Language) -> HighlightedCode {
|
pub fn highlight(source: &str, language: Language) -> HighlightedCode {
|
||||||
highlight_with_tree(source, language, None).0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Highlights `source`, reusing `old_tree` so only the changed region is re-lexed.
|
|
||||||
pub fn highlight_with_tree(
|
|
||||||
source: &str,
|
|
||||||
language: Language,
|
|
||||||
old_tree: Option<&tree_sitter::Tree>,
|
|
||||||
) -> (HighlightedCode, Option<tree_sitter::Tree>) {
|
|
||||||
let Some((grammar, query_source)) = grammar_for(language) else {
|
let Some((grammar, query_source)) = grammar_for(language) else {
|
||||||
return (plain(source), None);
|
return plain(source);
|
||||||
};
|
};
|
||||||
|
|
||||||
HIGHLIGHTERS.with(|cache| {
|
HIGHLIGHTERS.with(|cache| {
|
||||||
@@ -104,11 +95,11 @@ pub fn highlight_with_tree(
|
|||||||
.as_mut();
|
.as_mut();
|
||||||
|
|
||||||
let Some(highlighter) = highlighter else {
|
let Some(highlighter) = highlighter else {
|
||||||
return (plain(source), None);
|
return plain(source);
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(tree) = highlighter.parser.parse(source, old_tree) else {
|
let Some(tree) = highlighter.parser.parse(source, None) else {
|
||||||
return (plain(source), None);
|
return plain(source);
|
||||||
};
|
};
|
||||||
|
|
||||||
// One role per byte; matches arrive in query order
|
// One role per byte; matches arrive in query order
|
||||||
@@ -134,7 +125,7 @@ pub fn highlight_with_tree(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(build_lines(source, &role_at), Some(tree))
|
build_lines(source, &role_at)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, T
|
|||||||
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use crate::highlight::{HighlightedCode, highlight_with_tree, plain};
|
use crate::highlight::{HighlightedCode, highlight, plain};
|
||||||
use crate::utils::{code_line, grammar_language};
|
use crate::utils::{code_line, grammar_language};
|
||||||
|
|
||||||
/// Heading scale steps relative to the root font size, H1 first.
|
/// Heading scale steps relative to the root font size, H1 first.
|
||||||
@@ -277,7 +277,8 @@ impl Render for MarkdownViewState {
|
|||||||
.size_full()
|
.size_full()
|
||||||
.child(blocks)
|
.child(blocks)
|
||||||
.child(
|
.child(
|
||||||
Scrollbar::vertical(self.handle.clone()).visibility(ScrollbarVisibility::Platform),
|
Scrollbar::vertical((self.id.clone(), "v-scrollbar"), self.handle.clone())
|
||||||
|
.visibility(ScrollbarVisibility::Platform),
|
||||||
)
|
)
|
||||||
.when_some(self.a11y_label.clone(), |view, label| {
|
.when_some(self.a11y_label.clone(), |view, label| {
|
||||||
view.aria_label(label)
|
view.aria_label(label)
|
||||||
@@ -588,7 +589,7 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
|||||||
.then(|| language.as_deref().and_then(grammar_language))
|
.then(|| language.as_deref().and_then(grammar_language))
|
||||||
.flatten()
|
.flatten()
|
||||||
{
|
{
|
||||||
Some(grammar) => highlight_with_tree(text, grammar, None).0,
|
Some(grammar) => highlight(text, grammar),
|
||||||
None => plain(text),
|
None => plain(text),
|
||||||
};
|
};
|
||||||
blocks.push(MarkdownBlock::Code {
|
blocks.push(MarkdownBlock::Code {
|
||||||
|
|||||||
@@ -1016,7 +1016,7 @@ fn render_pane(
|
|||||||
move |_, _| {
|
move |_, _| {
|
||||||
tabs.iter()
|
tabs.iter()
|
||||||
.map(|tab| {
|
.map(|tab| {
|
||||||
let mut entry = Tab::new(tab.title.clone());
|
let mut entry = Tab::new(tab.panel.clone(), tab.title.clone());
|
||||||
if let Some(role) = tab.icon {
|
if let Some(role) = tab.icon {
|
||||||
entry = entry.icon(role);
|
entry = entry.icon(role);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,6 +255,14 @@ impl RenderOnce for ResizeHandle {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if event.pressed_button != Some(MouseButton::Left) {
|
||||||
|
drag.update(cx, |drag, cx| {
|
||||||
|
drag.dragging = false;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let delta = match axis {
|
let delta = match axis {
|
||||||
Axis::Horizontal => event.position.x - last.x,
|
Axis::Horizontal => event.position.x - last.x,
|
||||||
Axis::Vertical => event.position.y - last.y,
|
Axis::Vertical => event.position.y - last.y,
|
||||||
|
|||||||
@@ -151,6 +151,28 @@ fn moves_without_a_drag_report_nothing() {
|
|||||||
assert!(deltas.borrow().is_empty());
|
assert!(deltas.borrow().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The button may be released outside the window, where no MouseUp is
|
||||||
|
/// delivered — the drag must end when a move reports no pressed button.
|
||||||
|
#[test]
|
||||||
|
fn releasing_outside_the_window_ends_the_drag() {
|
||||||
|
let (_cx, deltas, _resets, mut visual) = handle_window(Axis::Horizontal, false);
|
||||||
|
let start = handle_center(&mut visual);
|
||||||
|
|
||||||
|
visual.simulate_mouse_down(start, MouseButton::Left, Modifiers::default());
|
||||||
|
visual.simulate_event(MouseMoveEvent {
|
||||||
|
position: point(start.x + px(30.), start.y),
|
||||||
|
pressed_button: None,
|
||||||
|
modifiers: Modifiers::default(),
|
||||||
|
});
|
||||||
|
visual.simulate_event(MouseMoveEvent {
|
||||||
|
position: point(start.x + px(60.), start.y),
|
||||||
|
pressed_button: None,
|
||||||
|
modifiers: Modifiers::default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(deltas.borrow().is_empty(), "the drag outlived its button");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arrow_keys_move_the_seam_when_focused() {
|
fn arrow_keys_move_the_seam_when_focused() {
|
||||||
let (_cx, deltas, _resets, mut visual) = handle_window(Axis::Horizontal, false);
|
let (_cx, deltas, _resets, mut visual) = handle_window(Axis::Horizontal, false);
|
||||||
|
|||||||
@@ -2309,10 +2309,10 @@ impl Render for TabsDemo {
|
|||||||
})
|
})
|
||||||
.tabs(|_, _| {
|
.tabs(|_, _| {
|
||||||
vec![
|
vec![
|
||||||
Tab::new("Overview"),
|
Tab::new("overview", "Overview"),
|
||||||
Tab::new("Details"),
|
Tab::new("details", "Details"),
|
||||||
Tab::new("Settings"),
|
Tab::new("settings", "Settings"),
|
||||||
Tab::new("Locked").disabled(true),
|
Tab::new("locked", "Locked").disabled(true),
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
@@ -2356,7 +2356,7 @@ impl Render for TabsDemo {
|
|||||||
labels
|
labels
|
||||||
.iter()
|
.iter()
|
||||||
.map(|label| {
|
.map(|label| {
|
||||||
Tab::new(label.clone())
|
Tab::new(label.clone(), label.clone())
|
||||||
.closeable(true)
|
.closeable(true)
|
||||||
.busy(label == "Syncing")
|
.busy(label == "Syncing")
|
||||||
})
|
})
|
||||||
@@ -3228,7 +3228,7 @@ impl Render for ScrollbarDemo {
|
|||||||
.h_full()
|
.h_full()
|
||||||
.w_full(),
|
.w_full(),
|
||||||
)
|
)
|
||||||
.child(Scrollbar::vertical(self.uniform.clone()).visibility(visibility)),
|
.child(Scrollbar::vertical("uniform", self.uniform.clone()).visibility(visibility)),
|
||||||
))
|
))
|
||||||
.child(demo_section(
|
.child(demo_section(
|
||||||
"list",
|
"list",
|
||||||
@@ -3254,7 +3254,7 @@ impl Render for ScrollbarDemo {
|
|||||||
.h_full()
|
.h_full()
|
||||||
.w_full(),
|
.w_full(),
|
||||||
)
|
)
|
||||||
.child(Scrollbar::vertical(self.list.clone()).visibility(visibility)),
|
.child(Scrollbar::vertical("list", self.list.clone()).visibility(visibility)),
|
||||||
))
|
))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
Reference in New Issue
Block a user