use super::{InlineToken, InlineTokenSpan, InputBaseState, InputModeKind, rope_ext::RopeExt as _};
use gpui::{AnyElement, App, Bounds, ClickEvent, Font, IntoElement, Pixels, Window};
use std::{collections::HashMap, ops::Range, rc::Rc};
#[derive(Clone)]
pub struct InlineTokenContext {
span: InlineTokenSpan,
selected: bool,
disabled: bool,
readonly: bool,
line_height: Pixels,
available_width: Pixels,
}
impl InlineTokenContext {
pub fn token(&self) -> &InlineToken {
self.span.token()
}
pub fn range(&self) -> Range<usize> {
self.span.range()
}
pub fn is_selected(&self) -> bool {
self.selected
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub fn is_readonly(&self) -> bool {
self.readonly
}
pub fn line_height(&self) -> Pixels {
self.line_height
}
pub fn available_width(&self) -> Pixels {
self.available_width
}
}
#[derive(Clone)]
pub struct InlineTokenClickEvent {
span: InlineTokenSpan,
bounds: Bounds<Pixels>,
event: ClickEvent,
}
impl InlineTokenClickEvent {
pub fn token(&self) -> &InlineToken {
self.span.token()
}
pub fn range(&self) -> Range<usize> {
self.span.range()
}
pub fn bounds(&self) -> Bounds<Pixels> {
self.bounds
}
pub fn click(&self) -> &ClickEvent {
&self.event
}
}
#[derive(Clone)]
pub struct InlineTokenHoverEvent {
span: InlineTokenSpan,
bounds: Bounds<Pixels>,
hovered: bool,
range_utf16: (usize, usize),
}
impl InlineTokenHoverEvent {
pub fn token(&self) -> &InlineToken {
self.span.token()
}
pub fn range(&self) -> Range<usize> {
self.span.range()
}
pub fn bounds(&self) -> Bounds<Pixels> {
self.bounds
}
pub fn is_hovered(&self) -> bool {
self.hovered
}
pub fn range_utf16(&self) -> (usize, usize) {
self.range_utf16
}
}
#[derive(Clone)]
pub(super) struct HoverSnapshot {
pub(super) span: InlineTokenSpan,
pub(super) bounds: Bounds<Pixels>,
pub(super) range_utf16: (usize, usize),
}
#[doc(hidden)]
pub type InlineTokenRenderer = Rc<dyn Fn(&InlineTokenContext, &mut Window, &mut App) -> AnyElement>;
#[doc(hidden)]
pub type InlineTokenClickListener = Rc<dyn Fn(&InlineTokenClickEvent, &mut Window, &mut App)>;
#[doc(hidden)]
pub type InlineTokenHoverListener = Rc<dyn Fn(&InlineTokenHoverEvent, &mut Window, &mut App)>;
#[derive(Clone, Default)]
pub(crate) struct InlineTokenPresentation {
renderer: Option<InlineTokenRenderer>,
listener: Option<InlineTokenClickListener>,
hover_listener: Option<InlineTokenHoverListener>,
secret: bool,
}
impl InlineTokenPresentation {
pub(crate) fn token<R: IntoElement>(
mut self,
render: impl Fn(&InlineTokenContext, &mut Window, &mut App) -> R + 'static,
) -> Self {
self.renderer = Some(Rc::new(move |token, window, cx| {
render(token, window, cx).into_any_element()
}));
self
}
pub(crate) fn on_token_click(
mut self,
listener: impl Fn(&InlineTokenClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.listener = Some(Rc::new(listener));
self
}
pub(crate) fn on_token_hover(
mut self,
listener: impl Fn(&InlineTokenHoverEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.hover_listener = Some(Rc::new(listener));
self
}
pub(super) fn has_hover_listener(&self) -> bool {
self.hover_listener.is_some()
}
pub(super) fn has_listener(&self) -> bool {
self.listener.is_some()
}
pub(super) fn render(
&self,
token: &InlineTokenContext,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
if let Some(render) = &self.renderer {
render(token, window, cx)
} else {
gpui::div()
.child(token.token().label().clone())
.into_any_element()
}
}
}
use gpui::ParentElement as _;
#[derive(Default)]
pub(super) struct TokenLayoutCache {
pub(super) key: Option<(Font, Pixels, Pixels, Pixels, bool)>,
pub(super) revision: u64,
pub(super) unwrapped_width: Pixels,
pub(super) metrics: Rc<[(Range<usize>, Pixels)]>,
pub(super) widths: HashMap<InlineToken, Pixels>,
}
impl<M: InputModeKind> InputBaseState<M> {
pub(crate) fn set_token_presentation(&mut self, presentation: InlineTokenPresentation) {
self.token_presentation = presentation;
}
#[doc(hidden)]
pub fn install_token_presentation(
&mut self,
renderer: Option<InlineTokenRenderer>,
listener: Option<InlineTokenClickListener>,
secret: bool,
) {
self.token_presentation = InlineTokenPresentation {
renderer,
listener,
hover_listener: self.token_presentation.hover_listener.clone(),
secret,
};
}
#[doc(hidden)]
pub fn install_token_hover_presentation(
&mut self,
hover_listener: Option<InlineTokenHoverListener>,
) {
self.token_presentation.hover_listener = hover_listener;
}
pub(super) fn tokens_visible(&self) -> bool {
!self.masked
&& !self.token_presentation.secret
&& self.mask_pattern.is_none()
&& !self.token_spans().is_empty()
}
pub(super) fn token_context(
&self,
span: &InlineTokenSpan,
line_height: Pixels,
width: Pixels,
) -> InlineTokenContext {
let range = span.range();
let selection = self.selected_range();
InlineTokenContext {
span: span.clone(),
selected: selection.start < range.end && range.start < selection.end,
disabled: self.disabled,
readonly: self.readonly,
line_height,
available_width: width,
}
}
pub(super) fn token_activation(
&self,
start: usize,
bounds: Bounds<Pixels>,
event: ClickEvent,
) -> Option<(InlineTokenClickListener, InlineTokenClickEvent)> {
if self.disabled || !self.tokens_visible() {
return None;
}
let span = self
.token_spans()
.iter()
.find(|span| span.range().start == start)?
.clone();
Some((
self.token_presentation.listener.clone()?,
InlineTokenClickEvent {
span,
bounds,
event,
},
))
}
pub(super) fn token_hover(
&mut self,
start: usize,
bounds: Bounds<Pixels>,
hovered: bool,
expected_token: Option<&InlineToken>,
) -> Option<(InlineTokenHoverListener, InlineTokenHoverEvent)> {
let listener = self.token_presentation.hover_listener.clone()?;
let matches = |snapshot: &HoverSnapshot| {
snapshot.span.range().start == start
&& expected_token.is_none_or(|token| snapshot.span.token() == token)
};
let snapshot = if hovered {
if self.disabled || !self.tokens_visible() {
return None;
}
let span = self
.token_spans()
.iter()
.find(|span| {
span.range().start == start
&& expected_token.is_none_or(|token| span.token() == token)
})?
.clone();
if self
.hovered_token
.as_ref()
.is_some_and(|snapshot| snapshot.span == span)
{
return None;
}
let range = span.range();
let snapshot = HoverSnapshot {
span,
bounds,
range_utf16: (
self.text.offset_to_offset_utf16(range.start),
self.text.offset_to_offset_utf16(range.end),
),
};
if let Some(previous) = self.hovered_token.replace(snapshot.clone()) {
self.pending_token_hover_exits.push(previous);
}
snapshot
} else if self.hovered_token.as_ref().is_some_and(matches) {
self.hovered_token.take()?
} else {
let index = self.pending_token_hover_exits.iter().position(matches)?;
self.pending_token_hover_exits.remove(index)
};
Some((
listener,
InlineTokenHoverEvent {
span: snapshot.span,
bounds: snapshot.bounds,
hovered,
range_utf16: snapshot.range_utf16,
},
))
}
pub(super) fn reconcile_token_hover(
&mut self,
) -> Option<(InlineTokenHoverListener, InlineTokenHoverEvent)> {
let is_placed = |snapshot: &HoverSnapshot| {
!self.disabled
&& self.tokens_visible()
&& self.token_presentation.has_hover_listener()
&& self.token_bounds.contains_key(&snapshot.span.range().start)
&& self.token_spans().contains(&snapshot.span)
};
let snapshot = if let Some(index) = self
.pending_token_hover_exits
.iter()
.position(|snapshot| !is_placed(snapshot))
{
self.pending_token_hover_exits.remove(index)
} else if self
.hovered_token
.as_ref()
.is_some_and(|snapshot| !is_placed(snapshot))
{
self.hovered_token.take()?
} else {
return None;
};
Some((
self.token_presentation.hover_listener.clone()?,
InlineTokenHoverEvent {
span: snapshot.span,
bounds: snapshot.bounds,
hovered: false,
range_utf16: snapshot.range_utf16,
},
))
}
pub(super) fn token_is_secret(&self) -> bool {
self.token_presentation.secret
}
}