StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | //! This element adds cross-element selectability to the UI framework. |
| 2 | //! |
| 3 | //! Any elements underneath a SelectableArea which both implement SelectableElement and |
| 4 | //! pass in a selectable state handle will be selectable underneath that SelectableArea. |
| 5 | //! |
| 6 | //! For an example of basic usage, refer to the selectable UI sample. |
| 7 | |
| 8 | use super::SelectionFragment; |
| 9 | use super::{ |
| 10 | AfterLayoutContext, AppContext, ColorU, Element, Event, EventContext, LayoutContext, |
| 11 | PaintContext, Point, SizeConstraint, |
| 12 | }; |
| 13 | use crate::event::{DispatchedEvent, ModifiersState}; |
| 14 | use crate::text::word_boundaries::WordBoundariesPolicy; |
| 15 | use crate::text::{IsRect, SelectionDirection, SelectionType}; |
| 16 | use pathfinder_geometry::vector::{vec2f, Vector2F}; |
| 17 | |
| 18 | use crate::text_offsets::ByteOffset; |
| 19 | use lazy_static::lazy_static; |
| 20 | use std::ops::Range; |
| 21 | use std::sync::Arc; |
| 22 | use std::sync::Mutex; |
| 23 | |
| 24 | /// A function that, given some content and a double-click index offset in that content, |
| 25 | /// returns the resulting smart selection range. |
| 26 | pub type SmartSelectFn = fn(content: &str, click_offset: ByteOffset) -> Option<Range<ByteOffset>>; |
| 27 | |
| 28 | pub struct SelectableArea { |
| 29 | child: Box<dyn Element>, |
| 30 | size: Option<Vector2F>, |
| 31 | origin: Option<Point>, |
| 32 | selection_handler: SelectionHandler, |
| 33 | selection_updated_handler: Option<SelectionUpdatedHandler>, |
| 34 | selection_right_click_handler: Option<SelectionRightClickHandler>, |
| 35 | |
| 36 | // To preserve selections when scrolling, the selectable area stores the current selection's |
| 37 | // state as points relative to the origin. When rendering the selection, these points |
| 38 | // are then converted back to absolute points using the origin. |
| 39 | selectable_area_state: SelectionHandle, |
| 40 | |
| 41 | word_boundaries_policy: WordBoundariesPolicy, |
| 42 | |
| 43 | smart_select_fn: Option<SmartSelectFn>, |
| 44 | |
| 45 | should_support_rect_select: bool, |
| 46 | } |
| 47 | |
| 48 | /// Stores the selection start and end points. We include the option to store |
| 49 | /// bounds alongside raw selection points since we may need to clamp the selection |
| 50 | /// points to the SelectableArea's bounds when it's not laid out (i.e. to support |
| 51 | /// across-block selections with AI blocks). |
| 52 | #[derive(Clone, Copy, Debug, Default)] |
| 53 | pub struct InternalSelection { |
| 54 | /// The point where the user first clicked before dragging. |
| 55 | /// This could be after tail if the selection is reversed. |
| 56 | pub head: Option<SelectionBound>, |
| 57 | /// The latest point the user dragged the selection to. |
| 58 | /// This could be before head if the selection is reversed. |
| 59 | pub tail: Option<SelectionBound>, |
| 60 | /// The head of the selection after semantic expansion. Note the direction of expansion |
| 61 | /// depends on whether the selection was reversed. |
| 62 | /// This could be after tail if the selection is reversed. |
| 63 | pub expanded_head: Option<SelectionBound>, |
| 64 | /// The tail of the selection after semantic expansion. Note the direction of expansion |
| 65 | /// depends on whether the selection was reversed. |
| 66 | /// This could be before head if the selection is reversed. |
| 67 | pub expanded_tail: Option<SelectionBound>, |
| 68 | /// The initial smart selection on double-click, set only if |
| 69 | /// smart_select_fn successfully returned a smart selection. |
| 70 | /// We store this separately because selection updates after dragging should never be smaller than |
| 71 | /// the initial smart selection range. |
| 72 | pub initial_smart_selection: Option<InitialSmartSelection>, |
| 73 | /// The semantic selection unit. |
| 74 | pub unit: SelectionType, |
| 75 | pub is_selecting: bool, |
| 76 | /// If true, head is after tail. |
| 77 | pub is_reversed: bool, |
| 78 | /// Whether we should return the smart selection's start when computing the selection start. |
| 79 | /// This is caching whether the smart selection's start is earlier than the expanded start. |
| 80 | pub should_use_smart_start: bool, |
| 81 | /// Whether we should return the smart selection's end when computing the selection end. |
| 82 | /// This is caching whether the smart selection's end is later than the expanded end. |
| 83 | pub should_use_smart_end: bool, |
| 84 | } |
| 85 | |
| 86 | /// The initial smart selection on double-click, set only if |
| 87 | /// smart_select_fn successfully returned a smart selection. |
| 88 | /// We store this separately because selection updates after dragging should never be smaller than |
| 89 | /// the initial smart selection range. |
| 90 | #[derive(Clone, Copy, Debug)] |
| 91 | pub struct InitialSmartSelection { |
| 92 | /// Always before end. |
| 93 | pub start: SelectionBound, |
| 94 | /// Always after start. |
| 95 | pub end: SelectionBound, |
| 96 | } |
| 97 | |
| 98 | impl InternalSelection { |
| 99 | // Returns the start point of the selection, using expanded points |
| 100 | // if they exist. This is always before end. |
| 101 | pub fn start(&self) -> Option<SelectionBound> { |
| 102 | if self.should_use_smart_start { |
| 103 | self.initial_smart_selection |
| 104 | .map(|smart_selection| smart_selection.start) |
| 105 | } else if self.is_reversed { |
| 106 | self.expanded_tail.or(self.tail) |
| 107 | } else { |
| 108 | self.expanded_head.or(self.head) |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | // Returns the end point of the selection, using expanded points |
| 113 | // if they exist. This is always after start. |
| 114 | pub fn end(&self) -> Option<SelectionBound> { |
| 115 | if self.should_use_smart_end { |
| 116 | self.initial_smart_selection |
| 117 | .map(|smart_selection| smart_selection.end) |
| 118 | } else if self.is_reversed { |
| 119 | self.expanded_head.or(self.head) |
| 120 | } else { |
| 121 | self.expanded_tail.or(self.tail) |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | /// Clears the current selection state. |
| 126 | /// |
| 127 | /// This is `pub` so callers may imperatively clear selection state in cases where a |
| 128 | /// selection-clearing mouse or keyboard event is handled prior to being received by this |
| 129 | /// `SelectableArea`. |
| 130 | pub fn clear(&mut self) { |
| 131 | *self = InternalSelection::default(); |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | #[derive(Clone, Copy, Debug, Default, PartialEq)] |
| 136 | pub struct Selection { |
| 137 | pub start: Vector2F, |
| 138 | pub end: Vector2F, |
| 139 | pub is_rect: IsRect, |
| 140 | } |
| 141 | |
| 142 | #[derive(Clone, Copy, Debug, PartialEq)] |
| 143 | pub enum SelectionBound { |
| 144 | /// Relative to the SelectableArea's origin |
| 145 | Relative(Vector2F), |
| 146 | /// Start from the top left point of the SelectableArea. |
| 147 | TopLeft, |
| 148 | /// Start from the bottom right of the SelectableArea. |
| 149 | BottomRight, |
| 150 | /// Start from the top column of the SelectableArea. The row is defined by the x_bound. |
| 151 | Top { x_bound: f32 }, |
| 152 | /// Start from the bottom column of the SelectableArea. The row is defined by the x_bound. |
| 153 | Bottom { x_bound: f32 }, |
| 154 | } |
| 155 | |
| 156 | impl SelectionBound { |
| 157 | fn as_absolute_point(&self, size: Vector2F, origin: Vector2F) -> Vector2F { |
| 158 | match self { |
| 159 | SelectionBound::Relative(point) => *point + origin, |
| 160 | SelectionBound::TopLeft => origin, |
| 161 | SelectionBound::BottomRight => origin + size, |
| 162 | SelectionBound::Top { x_bound } => vec2f(*x_bound, origin.y()), |
| 163 | SelectionBound::Bottom { x_bound } => vec2f(*x_bound, size.y() + origin.y()), |
| 164 | } |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | pub struct SelectionUpdateArgs { |
| 169 | pub selection: Option<String>, |
| 170 | } |
| 171 | |
| 172 | lazy_static! { |
| 173 | pub static ref SELECTED_HIGHLIGHT_COLOR: ColorU = |
| 174 | ColorU::new(118, 167, 250, (0.4 * 255.) as u8); |
| 175 | } |
| 176 | |
| 177 | #[derive(Default, Clone, Debug)] |
| 178 | pub struct SelectionHandle { |
| 179 | selection: Arc<Mutex<InternalSelection>>, |
| 180 | } |
| 181 | pub type SelectionHandler = Box<dyn FnMut(SelectionUpdateArgs, &mut EventContext, &AppContext)>; |
| 182 | type SelectionUpdatedHandler = Box<dyn FnMut(&mut EventContext, &AppContext)>; |
| 183 | type SelectionRightClickHandler = Box<dyn FnMut(&mut EventContext, Vector2F)>; |
| 184 | |
| 185 | impl SelectionHandle { |
| 186 | /// This isn't meant for general use. It's used specifically in cases where a selection is started |
| 187 | /// outside the SelectableArea's bounds and the SelectableArea's start point needs to be clamped manually. |
| 188 | pub fn start_selection_outside(&self, bound: SelectionBound, unit: SelectionType) { |
| 189 | let mut selection = self.selection.lock().expect("Should not be poisoned."); |
| 190 | selection.head = Some(bound); |
| 191 | selection.unit = unit; |
| 192 | selection.is_selecting = true; |
| 193 | } |
| 194 | |
| 195 | /// Whether there is an active selection in the SelectableArea. |
| 196 | /// An active selection is not necessarily a non-empty selection. |
| 197 | pub fn is_selecting(&self) -> bool { |
| 198 | self.selection |
| 199 | .lock() |
| 200 | .expect("Should not be poisoned.") |
| 201 | .is_selecting |
| 202 | } |
| 203 | |
| 204 | pub fn clear(&self) { |
| 205 | self.selection |
| 206 | .lock() |
| 207 | .expect("Mutex is not poisoned.") |
| 208 | .clear(); |
| 209 | } |
| 210 | |
| 211 | #[cfg(feature = "integration_tests")] |
| 212 | pub fn selection_type(&self) -> SelectionType { |
| 213 | self.selection.lock().expect("Mutex is not poisoned.").unit |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | impl SelectableArea { |
| 218 | pub fn new<F>( |
| 219 | selectable_area_state: SelectionHandle, |
| 220 | selection_handler: F, |
| 221 | child: Box<dyn Element>, |
| 222 | ) -> Self |
| 223 | where |
| 224 | F: 'static + FnMut(SelectionUpdateArgs, &mut EventContext, &AppContext), |
| 225 | { |
| 226 | Self { |
| 227 | child, |
| 228 | size: None, |
| 229 | origin: None, |
| 230 | selectable_area_state, |
| 231 | selection_handler: Box::new(selection_handler), |
| 232 | selection_updated_handler: None, |
| 233 | selection_right_click_handler: None, |
| 234 | word_boundaries_policy: WordBoundariesPolicy::Default, |
| 235 | smart_select_fn: None, |
| 236 | should_support_rect_select: false, |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | pub fn should_support_rect_select(mut self) -> Self { |
| 241 | self.should_support_rect_select = true; |
| 242 | self |
| 243 | } |
| 244 | |
| 245 | pub fn with_word_boundaries_policy(self, word_boundaries_policy: WordBoundariesPolicy) -> Self { |
| 246 | Self { |
| 247 | word_boundaries_policy, |
| 248 | ..self |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | pub fn with_smart_select_fn(self, smart_select_fn: Option<SmartSelectFn>) -> Self { |
| 253 | Self { |
| 254 | smart_select_fn, |
| 255 | ..self |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | /// The selection updated handler is invoked only when a selection is actively being made. |
| 260 | /// Clearing the text selection in a `SelectableArea` via `LeftMouseDown` doesn't count. |
| 261 | pub fn on_selection_updated<F>(self, selection_updated_handler: F) -> Self |
| 262 | where |
| 263 | F: 'static + FnMut(&mut EventContext, &AppContext), |
| 264 | { |
| 265 | Self { |
| 266 | selection_updated_handler: Some(Box::new(selection_updated_handler)), |
| 267 | ..self |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | pub fn on_selection_right_click<F>(self, selection_right_click_fn: F) -> Self |
| 272 | where |
| 273 | F: 'static + FnMut(&mut EventContext, Vector2F), |
| 274 | { |
| 275 | Self { |
| 276 | selection_right_click_handler: Some(Box::new(selection_right_click_fn)), |
| 277 | ..self |
| 278 | } |
| 279 | } |
| 280 | |
| 281 | /// Clears any existing selection, and starts a new selection if the click is in the element. |
| 282 | /// Does not handle cases where a selection is started outside the `SelectableArea`'s bounds. |
| 283 | /// Returns `true` if a new selection was successfully started. |
| 284 | fn on_mouse_down( |
| 285 | &mut self, |
| 286 | position: Vector2F, |
| 287 | modifiers: &ModifiersState, |
| 288 | click_count: u32, |
| 289 | ctx: &mut EventContext, |
| 290 | app: &AppContext, |
| 291 | ) -> bool { |
| 292 | let Some(selectable_child_ref) = self.child.as_selectable_element() else { |
| 293 | return false; |
| 294 | }; |
| 295 | // Clear any previously existing selection on mouse down. |
| 296 | let mut selection_state = self |
| 297 | .selectable_area_state |
| 298 | .selection |
| 299 | .lock() |
| 300 | .expect("Should not be poisoned."); |
| 301 | selection_state.clear(); |
| 302 | |
| 303 | // Only if this click was in the element, start a new selection. |
| 304 | if !is_mouse_in(self.origin, self.size, ctx, position) { |
| 305 | return false; |
| 306 | } |
| 307 | let Some(origin) = self.origin else { |
| 308 | return false; |
| 309 | }; |
| 310 | selection_state.is_selecting = true; |
| 311 | |
| 312 | let unit = if click_count == 1 { |
| 313 | if self.should_support_rect_select && modifiers.alt && modifiers.cmd { |
| 314 | SelectionType::Rect |
| 315 | } else { |
| 316 | SelectionType::Simple |
| 317 | } |
| 318 | } else if click_count == 2 { |
| 319 | SelectionType::Semantic |
| 320 | } else { |
| 321 | SelectionType::Lines |
| 322 | }; |
| 323 | selection_state.unit = unit; |
| 324 | selection_state.head = Some(SelectionBound::Relative(position - origin.xy)); |
| 325 | selection_state.tail = Some(SelectionBound::Relative(position - origin.xy)); |
| 326 | |
| 327 | // First, try smart selection if it's configured and this is a double click. |
| 328 | let smart_select_range = match (unit, self.smart_select_fn) { |
| 329 | (SelectionType::Semantic, Some(smart_select_fn)) => { |
| 330 | selectable_child_ref.smart_select(position, smart_select_fn) |
| 331 | } |
| 332 | // In all other cases, use the default selection expansion |
| 333 | _ => None, |
| 334 | }; |
| 335 | let (expanded_head, expanded_tail) = match smart_select_range { |
| 336 | // If smart_select_range is set, use that as expanded head / tail. |
| 337 | Some((start, end)) => { |
| 338 | selection_state.initial_smart_selection = Some(InitialSmartSelection { |
| 339 | start: SelectionBound::Relative(start - origin.xy), |
| 340 | end: SelectionBound::Relative(end - origin.xy), |
| 341 | }); |
| 342 | (Some(start), Some(end)) |
| 343 | } |
| 344 | _ => { |
| 345 | // Otherwise, expand the selection normally. |
| 346 | let expanded_head = selectable_child_ref.expand_selection( |
| 347 | position, |
| 348 | SelectionDirection::Backward, |
| 349 | unit, |
| 350 | &self.word_boundaries_policy, |
| 351 | ); |
| 352 | let expanded_tail = selectable_child_ref.expand_selection( |
| 353 | position, |
| 354 | SelectionDirection::Forward, |
| 355 | unit, |
| 356 | &self.word_boundaries_policy, |
| 357 | ); |
| 358 | (expanded_head, expanded_tail) |
| 359 | } |
| 360 | }; |
| 361 | |
| 362 | // Set the expanded head and tail. Since the resulting expanded selection could be |
| 363 | // non-empty, we should invoke the selection update handler as well. |
| 364 | if let Some((head, tail)) = expanded_head.zip(expanded_tail) { |
| 365 | selection_state.expanded_head = Some(SelectionBound::Relative(head - origin.xy)); |
| 366 | selection_state.expanded_tail = Some(SelectionBound::Relative(tail - origin.xy)); |
| 367 | |
| 368 | if head != tail { |
| 369 | if let Some(selection_updated_handler) = self.selection_updated_handler.as_mut() { |
| 370 | selection_updated_handler(ctx, app); |
| 371 | } |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | // By this point, we've determined that a selection has successfully started. |
| 376 | // Returning `true` ensures that parent `SelectableArea`s don't attempt to start their |
| 377 | // own text selections at the same time. |
| 378 | true |
| 379 | } |
| 380 | |
| 381 | fn on_right_mouse_down(&mut self, position: Vector2F, ctx: &mut EventContext) -> bool { |
| 382 | // Ignore this right-click unless it took place within this SelectableArea element |
| 383 | if !is_mouse_in(self.origin, self.size, ctx, position) { |
| 384 | return false; |
| 385 | } |
| 386 | |
| 387 | let current_selection = self.get_current_selection_absolute(); |
| 388 | let Some(selectable_child_ref) = self.child.as_selectable_element() else { |
| 389 | return false; |
| 390 | }; |
| 391 | let Some(right_click_handler) = self.selection_right_click_handler.as_mut() else { |
| 392 | return false; |
| 393 | }; |
| 394 | |
| 395 | let clickable_bounds = selectable_child_ref.calculate_clickable_bounds(current_selection); |
| 396 | let is_within_bounds = clickable_bounds |
| 397 | .iter() |
| 398 | .any(|bounds| bounds.contains_point(position)); |
| 399 | |
| 400 | if is_within_bounds { |
| 401 | let origin = self |
| 402 | .origin |
| 403 | .expect("Origin should be defined before mouse clicks") |
| 404 | .xy(); |
| 405 | let position_in_block = position - origin; |
| 406 | right_click_handler(ctx, position_in_block); |
| 407 | } |
| 408 | is_within_bounds |
| 409 | } |
| 410 | |
| 411 | /// Updates the selection using the latest tail position (where the user dragged to). |
| 412 | /// Expands selections as needed and computes whether the selection is_reversed. |
| 413 | /// Returns whether the selection was actually updated. |
| 414 | fn update_selection(&mut self, tail_absolute_position: Vector2F) -> bool { |
| 415 | let Some(selectable_child_ref) = self.child.as_selectable_element() else { |
| 416 | return false; |
| 417 | }; |
| 418 | let mut selection_state = self |
| 419 | .selectable_area_state |
| 420 | .selection |
| 421 | .lock() |
| 422 | .expect("Should not be poisoned."); |
| 423 | |
| 424 | // We can only update the selection if we have a selection head the selection was initiated from. |
| 425 | let (Some(relative_selection_head), Some(origin), Some(size)) = |
| 426 | (selection_state.head, self.origin, self.size) |
| 427 | else { |
| 428 | return false; |
| 429 | }; |
| 430 | |
| 431 | let new_selection_tail = SelectionBound::Relative(tail_absolute_position - origin.xy); |
| 432 | // Don't update or cache the selection if it hasn't changed |
| 433 | if selection_state |
| 434 | .tail |
| 435 | .is_some_and(|old_selection_tail| old_selection_tail == new_selection_tail) |
| 436 | { |
| 437 | return false; |
| 438 | } |
| 439 | |
| 440 | // Update the selection's raw end. |
| 441 | selection_state.tail = Some(new_selection_tail); |
| 442 | |
| 443 | // Compute whether the selection is reversed. This is needed |
| 444 | // to determine how to expand the selection. |
| 445 | let head_absolute_position = relative_selection_head.as_absolute_point(size, origin.xy()); |
| 446 | let is_reversed = if matches!( |
| 447 | relative_selection_head, |
| 448 | SelectionBound::TopLeft | SelectionBound::Top { .. } |
| 449 | ) { |
| 450 | Some(false) |
| 451 | } else if matches!( |
| 452 | relative_selection_head, |
| 453 | SelectionBound::BottomRight | SelectionBound::Bottom { .. } |
| 454 | ) { |
| 455 | Some(true) |
| 456 | } else { |
| 457 | // If the end is before the start, this is a reversed selection. |
| 458 | selectable_child_ref |
| 459 | .is_point_semantically_before(tail_absolute_position, head_absolute_position) |
| 460 | }; |
| 461 | // If we can't tell whether the selection is reversed, don't do semantic expansion. |
| 462 | let Some(is_reversed_selection) = is_reversed else { |
| 463 | // We return true here because the selection was already successfully updated with the latest unexpanded tail. |
| 464 | return true; |
| 465 | }; |
| 466 | |
| 467 | let (head_direction, tail_direction) = if is_reversed_selection { |
| 468 | // If this is a reversed selection, the tail (point the user dragged to) should be expanded backward |
| 469 | // since it will be the start of the selection. |
| 470 | (SelectionDirection::Forward, SelectionDirection::Backward) |
| 471 | } else { |
| 472 | // If this is a forward selection, the head (point user originally clicked before dragging) |
| 473 | // should be expanded backward since it will be the start of the selection. |
| 474 | (SelectionDirection::Backward, SelectionDirection::Forward) |
| 475 | }; |
| 476 | |
| 477 | // We always need to expand the new tail. |
| 478 | // If we're changing the value of is_reversed, we also need to re-expand |
| 479 | // the head, since the direction of head expansion changes. |
| 480 | // There are expected cases where only one is expanded successfully and not the other. |
| 481 | // For example, if a semantic selection was started outside the selectable area and then |
| 482 | // dragged in, the original head would be a max/min bound of the selectable area which |
| 483 | // can't always be expanded. |
| 484 | let expanded_tail = selectable_child_ref.expand_selection( |
| 485 | tail_absolute_position, |
| 486 | tail_direction, |
| 487 | selection_state.unit, |
| 488 | &self.word_boundaries_policy, |
| 489 | ); |
| 490 | selection_state.expanded_tail = |
| 491 | expanded_tail.map(|expanded_tail| SelectionBound::Relative(expanded_tail - origin.xy)); |
| 492 | if selection_state.is_reversed != is_reversed_selection { |
| 493 | let expanded_head = selectable_child_ref.expand_selection( |
| 494 | head_absolute_position, |
| 495 | head_direction, |
| 496 | selection_state.unit, |
| 497 | &self.word_boundaries_policy, |
| 498 | ); |
| 499 | selection_state.expanded_head = expanded_head |
| 500 | .map(|expanded_head| SelectionBound::Relative(expanded_head - origin.xy)); |
| 501 | } |
| 502 | selection_state.is_reversed = is_reversed_selection; |
| 503 | // Now that we've set the new expanded head and tail, make sure our new selection is not smaller than |
| 504 | // the original smart selection if there was one. |
| 505 | // First reset the cached values to get the selection start/end without considering the initial smart selection. |
| 506 | selection_state.should_use_smart_start = false; |
| 507 | selection_state.should_use_smart_end = false; |
| 508 | let (Some(new_start), Some(new_end)) = (selection_state.start(), selection_state.end()) |
| 509 | else { |
| 510 | return true; |
| 511 | }; |
| 512 | let Some(initial_smart_selection) = selection_state.initial_smart_selection else { |
| 513 | return true; |
| 514 | }; |
| 515 | // Use the smart selection start/end if they would make the selection range bigger than the expanded selection. |
| 516 | if selectable_child_ref |
| 517 | .is_point_semantically_before( |
| 518 | initial_smart_selection |
| 519 | .start |
| 520 | .as_absolute_point(size, origin.xy()), |
| 521 | new_start.as_absolute_point(size, origin.xy()), |
| 522 | ) |
| 523 | .unwrap_or(false) |
| 524 | { |
| 525 | selection_state.should_use_smart_start = true |
| 526 | } |
| 527 | if selectable_child_ref |
| 528 | .is_point_semantically_before( |
| 529 | new_end.as_absolute_point(size, origin.xy()), |
| 530 | initial_smart_selection |
| 531 | .end |
| 532 | .as_absolute_point(size, origin.xy()), |
| 533 | ) |
| 534 | .unwrap_or(false) |
| 535 | { |
| 536 | selection_state.should_use_smart_end = true |
| 537 | } |
| 538 | true |
| 539 | } |
| 540 | |
| 541 | // Returns the current selection in absolute coordinates. |
| 542 | fn get_current_selection_absolute(&self) -> Option<Selection> { |
| 543 | let (Some(origin), Some(size)) = (self.origin, self.size) else { |
| 544 | return None; |
| 545 | }; |
| 546 | let selection = self |
| 547 | .selectable_area_state |
| 548 | .selection |
| 549 | .lock() |
| 550 | .expect("Should not be poisoned."); |
| 551 | let (Some(start), Some(end)) = (selection.start(), selection.end()) else { |
| 552 | return None; |
| 553 | }; |
| 554 | |
| 555 | Some(Selection { |
| 556 | start: start.as_absolute_point(size, origin.xy()), |
| 557 | end: end.as_absolute_point(size, origin.xy()), |
| 558 | is_rect: selection.unit.into(), |
| 559 | }) |
| 560 | } |
| 561 | |
| 562 | fn get_current_selection_text_fragments(&self) -> Option<Vec<SelectionFragment>> { |
| 563 | let updated_selection = self.get_current_selection_absolute()?; |
| 564 | let selectable_child_ref = self.child.as_selectable_element()?; |
| 565 | |
| 566 | // Order selected text fragments |
| 567 | selectable_child_ref.get_selection( |
| 568 | updated_selection.start, |
| 569 | updated_selection.end, |
| 570 | updated_selection.is_rect, |
| 571 | ) |
| 572 | } |
| 573 | |
| 574 | fn is_current_selection_empty(&self) -> bool { |
| 575 | self.get_current_selection_text_fragments() |
| 576 | .unwrap_or_default() |
| 577 | .is_empty() |
| 578 | } |
| 579 | |
| 580 | fn invoke_selection_handler(&mut self, ctx: &mut EventContext, app: &AppContext) { |
| 581 | let text_fragments = self.get_current_selection_text_fragments(); |
| 582 | let update_args = SelectionUpdateArgs { |
| 583 | // If `text_fragments` is `None`, we still need to invoke the selection_handler accordingly. |
| 584 | // Otherwise, clicking away from text within an AIBlock won't clear the underlying selected_text state. |
| 585 | selection: text_fragments.map(order_and_concatenate_fragments), |
| 586 | }; |
| 587 | (self.selection_handler)(update_args, ctx, app); |
| 588 | ctx.notify(); |
| 589 | } |
| 590 | } |
| 591 | |
| 592 | /// Determine if the mouse is over the element |
| 593 | fn is_mouse_in( |
| 594 | origin: Option<Point>, |
| 595 | size: Option<Vector2F>, |
| 596 | ctx: &EventContext, |
| 597 | position: Vector2F, |
| 598 | ) -> bool { |
| 599 | let Some(origin) = origin else { |
| 600 | log::warn!("self.origin was None in `SelectableArea::is_mouse_in`"); |
| 601 | return false; |
| 602 | }; |
| 603 | let Some(size) = size else { |
| 604 | log::warn!("self.size() was None in `SelectableArea::is_mouse_in`"); |
| 605 | return false; |
| 606 | }; |
| 607 | |
| 608 | ctx.visible_rect(origin, size) |
| 609 | .is_some_and(|bound| bound.contains_point(position)) |
| 610 | } |
| 611 | |
| 612 | fn order_and_concatenate_fragments(mut selection_fragments: Vec<SelectionFragment>) -> String { |
| 613 | selection_fragments.sort_by(|a, b| { |
| 614 | if a.origin.y() == b.origin.y() { |
| 615 | a.origin.x().total_cmp(&b.origin.x()) |
| 616 | } else { |
| 617 | a.origin.y().total_cmp(&b.origin.y()) |
| 618 | } |
| 619 | }); |
| 620 | |
| 621 | selection_fragments |
| 622 | .iter() |
| 623 | .map(|s| s.text.as_str()) |
| 624 | .collect::<Vec<&str>>() |
| 625 | .concat() |
| 626 | } |
| 627 | |
| 628 | impl Element for SelectableArea { |
| 629 | fn layout( |
| 630 | &mut self, |
| 631 | constraint: SizeConstraint, |
| 632 | ctx: &mut LayoutContext, |
| 633 | app: &AppContext, |
| 634 | ) -> Vector2F { |
| 635 | let child_constraint = SizeConstraint { |
| 636 | min: (constraint.min).max(Vector2F::zero()), |
| 637 | max: (constraint.max).max(Vector2F::zero()), |
| 638 | }; |
| 639 | let child_size = self.child.layout(child_constraint, ctx, app); |
| 640 | let size = child_size; |
| 641 | self.size = Some(size); |
| 642 | size |
| 643 | } |
| 644 | |
| 645 | fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) { |
| 646 | self.child.after_layout(ctx, app); |
| 647 | } |
| 648 | |
| 649 | fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) { |
| 650 | self.origin = Some(Point::from_vec2f(origin, ctx.scene.z_index())); |
| 651 | ctx.current_selection = self.get_current_selection_absolute(); |
| 652 | self.child.paint(origin, ctx, app); |
| 653 | ctx.current_selection = None; |
| 654 | } |
| 655 | |
| 656 | fn dispatch_event( |
| 657 | &mut self, |
| 658 | event: &DispatchedEvent, |
| 659 | ctx: &mut EventContext, |
| 660 | app: &AppContext, |
| 661 | ) -> bool { |
| 662 | // Only dispatch to the child if we're not in the middle of a non-empty selection. |
| 663 | // Nested `SelectableArea` elements should pick up on mouse events if said events |
| 664 | // are not being used to create a non-trivial selection in this `SelectableArea`. |
| 665 | // Do not handle the event with this element if the child handles it, as doing so |
| 666 | // could result in nested `SelectableArea` elements being unnecessarily cleared. |
| 667 | let should_dispatch_to_child = |
| 668 | !self.selectable_area_state.is_selecting() || self.is_current_selection_empty(); |
| 669 | if should_dispatch_to_child { |
| 670 | let handled = self.child.as_mut().dispatch_event(event, ctx, app); |
| 671 | if handled { |
| 672 | return true; |
| 673 | } |
| 674 | } |
| 675 | |
| 676 | match event.raw_event() { |
| 677 | Event::LeftMouseDown { |
| 678 | position, |
| 679 | click_count, |
| 680 | modifiers, |
| 681 | .. |
| 682 | } => { |
| 683 | let selection_started = |
| 684 | self.on_mouse_down(*position, modifiers, *click_count, ctx, app); |
| 685 | |
| 686 | // Invoking the selection handler is necessary to notify parent views that we've |
| 687 | // cleared the internal selection state of the `SelectableArea`. |
| 688 | self.invoke_selection_handler(ctx, app); |
| 689 | selection_started |
| 690 | } |
| 691 | Event::LeftMouseDragged { position, .. } => { |
| 692 | if !self.selectable_area_state.is_selecting() { |
| 693 | return false; |
| 694 | } |
| 695 | let (Some(origin), Some(size)) = (self.origin, self.size) else { |
| 696 | return false; |
| 697 | }; |
| 698 | |
| 699 | let selection_updated = self.update_selection(*position); |
| 700 | if !selection_updated { |
| 701 | return false; |
| 702 | } |
| 703 | if let Some(selection_updated_handler) = self.selection_updated_handler.as_mut() { |
| 704 | selection_updated_handler(ctx, app); |
| 705 | } |
| 706 | |
| 707 | // Materialize and cache the selected text if SelectableArea is about to go off-screen. |
| 708 | // Since origin isn't available when SelectableArea is off-screen, we aren't able to |
| 709 | // materialize the selection on mouse up if that's the case. As a workaround, |
| 710 | // we cache it here ahead of time. |
| 711 | if origin.y() < 0. |
| 712 | || origin.y() + size.y() > app.windows().active_display_bounds().height() |
| 713 | { |
| 714 | self.invoke_selection_handler(ctx, app) |
| 715 | } |
| 716 | |
| 717 | // Returning true ensures that this SelectableArea's ongoing selections won't |
| 718 | // conflict with parent or child SelectableAreas in the element tree. |
| 719 | ctx.notify(); |
| 720 | true |
| 721 | } |
| 722 | Event::LeftMouseUp { position, .. } => { |
| 723 | self.selectable_area_state |
| 724 | .selection |
| 725 | .lock() |
| 726 | .expect("Should not be poisoned.") |
| 727 | .is_selecting = false; |
| 728 | self.invoke_selection_handler(ctx, app); |
| 729 | |
| 730 | // If the mouse is inside this element no other element needs to handle this event |
| 731 | // because this is the "lowest level" element so we return `true`. If the mouse is |
| 732 | // outside this element we return `false` so other elements can handle the event |
| 733 | // as well. We need to handle `LeftMouseUp` in either case to support selections |
| 734 | // across elements. Note that this behavior may need to change in the future. |
| 735 | is_mouse_in(self.origin, self.size, ctx, *position) |
| 736 | } |
| 737 | Event::RightMouseDown { position, .. } => self.on_right_mouse_down(*position, ctx), |
| 738 | _ => false, |
| 739 | } |
| 740 | } |
| 741 | |
| 742 | fn size(&self) -> Option<Vector2F> { |
| 743 | self.size |
| 744 | } |
| 745 | |
| 746 | fn origin(&self) -> Option<Point> { |
| 747 | self.origin |
| 748 | } |
| 749 | } |
| 750 |