gpui_kit_grid/state/
link.rs1use std::rc::Rc;
14
15use gpui_kit::{App, Bounds, Context, Modifiers, MouseButton, Pixels, Point, Window};
16
17use super::{ColumnOf, KeyOf, LiveGridDelegate, LiveGridState, menu::GridTarget};
18use crate::{
19 actions::OpenLink,
20 model::{CellValue, GridRow},
21};
22
23pub trait LinkOpener: 'static {
29 fn open(&self, url: &str, cx: &mut App);
31}
32
33#[derive(Clone, Copy, Debug, Default)]
36pub struct OpenInBrowser;
37
38impl LinkOpener for OpenInBrowser {
39 fn open(&self, url: &str, cx: &mut App) {
40 cx.open_url(url);
41 }
42}
43
44pub(super) struct LinkSlot {
47 opener: Rc<dyn LinkOpener>,
48 pub(super) rects: Vec<Bounds<Pixels>>,
50 pub(super) hovered: bool,
52 pub(super) series_on_link: bool,
55}
56
57impl Default for LinkSlot {
58 fn default() -> Self {
59 Self {
60 opener: Rc::new(OpenInBrowser),
61 rects: Vec::new(),
62 hovered: false,
63 series_on_link: false,
64 }
65 }
66}
67
68impl LinkSlot {
69 pub(super) fn is_over(&self, position: Point<Pixels>) -> bool {
71 self.rects.iter().any(|rect| rect.contains(&position))
72 }
73}
74
75impl<D: LiveGridDelegate> LiveGridState<D> {
76 pub fn with_link_opener(mut self, opener: impl LinkOpener) -> Self {
79 self.links.opener = Rc::new(opener);
80 self
81 }
82
83 pub fn is_link_hovered(&self) -> bool {
86 self.links.hovered
87 }
88
89 pub(super) fn is_pointer_on_link(&self) -> bool {
91 self.tooltip
92 .position
93 .is_some_and(|position| self.links.is_over(position))
94 }
95
96 pub(super) fn update_link_hover(
99 &mut self,
100 position: Option<Point<Pixels>>,
101 cx: &mut Context<Self>,
102 ) {
103 let hovered = position.is_some_and(|position| self.links.is_over(position));
104 if hovered != self.links.hovered {
105 self.links.hovered = hovered;
106 cx.notify();
107 }
108 }
109
110 fn link_url(&self, target: &GridTarget<KeyOf<D>, ColumnOf<D>>) -> Option<String> {
113 let (row, column) = match target {
114 GridTarget::Cell { key, column } => (self.snapshot.get(key)?.clone(), *column),
115 GridTarget::Pinned { edge, key, column } => (
116 self.snapshot
117 .pinned_rows(*edge)
118 .iter()
119 .find(|row| row.key() == key)?
120 .clone(),
121 *column,
122 ),
123 _ => return None,
124 };
125 match row.value(column) {
126 CellValue::Link(link) => Some(link.url().to_owned()),
127 _ => None,
128 }
129 }
130
131 fn open_link_of(
133 &self,
134 target: &GridTarget<KeyOf<D>, ColumnOf<D>>,
135 cx: &mut Context<Self>,
136 ) -> bool {
137 let Some(url) = self.link_url(target) else {
138 return false;
139 };
140 let opener = self.links.opener.clone();
141 cx.defer(move |cx| opener.open(&url, cx));
142 true
143 }
144
145 pub(super) fn open_clicked_link(
148 &self,
149 target: &GridTarget<KeyOf<D>, ColumnOf<D>>,
150 button: MouseButton,
151 modifiers: Modifiers,
152 click_count: usize,
153 origin: Point<Pixels>,
154 cx: &mut Context<Self>,
155 ) {
156 if button == MouseButton::Left
157 && click_count == 1
158 && !modifiers.modified()
159 && self.links.is_over(origin)
160 {
161 self.open_link_of(target, cx);
162 }
163 }
164
165 pub(super) fn is_link_focused(&self) -> bool {
168 self.focused_link_target()
169 .is_some_and(|target| self.link_url(&target).is_some())
170 }
171
172 fn focused_link_target(&self) -> Option<GridTarget<KeyOf<D>, ColumnOf<D>>> {
173 let focus = self.selection.focus()?;
174 Some(GridTarget::Cell {
175 key: focus.row()?.clone(),
176 column: focus.column(),
177 })
178 }
179
180 pub(super) fn bind_link_actions<E: gpui_kit::InteractiveElement>(
182 root: E,
183 cx: &Context<Self>,
184 ) -> E {
185 root.on_action(cx.listener(Self::on_open_link))
186 }
187
188 fn on_open_link(&mut self, _: &OpenLink, _: &mut Window, cx: &mut Context<Self>) {
191 let opened = self
192 .focused_link_target()
193 .is_some_and(|target| self.open_link_of(&target, cx));
194 if !opened {
195 cx.propagate();
196 }
197 }
198}