use std::cell::Cell;
use std::sync::{Arc, Mutex};
use kurbo::{Rect, Size};
use crate::event::PassBracket;
use crate::view::AnyView;
thread_local! {
static PUBLISHED: Cell<Option<SelectionToolbarRequest>> = const { Cell::new(None) };
static PASS_OPEN: Cell<bool> = const { Cell::new(false) };
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct SelectionToolbarActions {
pub copy: bool,
pub cut: bool,
pub paste: bool,
pub select_all: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SelectionToolbarRequest {
pub anchor: Rect,
pub actions: SelectionToolbarActions,
pub present_menu: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum SelectionToolbarPolicy {
#[default]
Framework,
Native,
}
#[derive(Clone, Copy, Debug)]
struct PolicyState {
value: SelectionToolbarPolicy,
locked: bool,
}
static POLICY: Mutex<PolicyState> = Mutex::new(PolicyState {
value: SelectionToolbarPolicy::Framework,
locked: false,
});
pub fn set_selection_toolbar_policy(policy: SelectionToolbarPolicy) {
let mut state = POLICY.lock().unwrap_or_else(|e| e.into_inner());
if state.locked {
#[cfg(debug_assertions)]
eprintln!(
"frust-core: the selection-toolbar policy is locked to {:?} by the platform shell; \
ignoring an app's set_selection_toolbar_policy({policy:?}) call (see \
frust_core::lock_selection_toolbar_policy)",
state.value
);
return;
}
state.value = policy;
}
pub fn selection_toolbar_policy() -> SelectionToolbarPolicy {
POLICY.lock().unwrap_or_else(|e| e.into_inner()).value
}
pub fn lock_selection_toolbar_policy(policy: SelectionToolbarPolicy) -> bool {
let mut state = POLICY.lock().unwrap_or_else(|e| e.into_inner());
if state.locked {
return false;
}
state.value = policy;
state.locked = true;
true
}
pub type SelectionToolbarBuilder =
Arc<dyn Fn(&SelectionToolbarRequest, Size) -> AnyView<()> + Send + Sync>;
static BUILDER: Mutex<Option<SelectionToolbarBuilder>> = Mutex::new(None);
pub fn set_selection_toolbar_builder(builder: SelectionToolbarBuilder) {
*BUILDER.lock().unwrap_or_else(|e| e.into_inner()) = Some(builder);
}
pub fn install_selection_toolbar_builder_if_unset(builder: SelectionToolbarBuilder) -> bool {
let mut slot = BUILDER.lock().unwrap_or_else(|e| e.into_inner());
if slot.is_some() {
return false;
}
*slot = Some(builder);
true
}
pub fn selection_toolbar_builder() -> Option<SelectionToolbarBuilder> {
BUILDER
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
.map(Arc::clone)
}
pub(crate) struct SelectionToolbarPass(PassBracket<Option<SelectionToolbarRequest>>);
impl SelectionToolbarPass {
pub(crate) fn enter() -> Self {
SelectionToolbarPass(PassBracket::enter(&PUBLISHED, &PASS_OPEN))
}
pub(crate) fn take(&self) -> Option<SelectionToolbarRequest> {
self.0.take()
}
}
pub(crate) fn publish(request: SelectionToolbarRequest) {
PUBLISHED.with(|slot| slot.set(Some(request)));
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex as StdMutex;
static TEST_LOCK: StdMutex<()> = StdMutex::new(());
fn toolbar_request() -> SelectionToolbarRequest {
SelectionToolbarRequest {
anchor: Rect::new(10.0, 20.0, 110.0, 40.0),
actions: SelectionToolbarActions {
copy: true,
cut: true,
paste: false,
select_all: true,
},
present_menu: true,
}
}
#[test]
fn policy_defaults_to_framework_and_round_trips() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
assert_eq!(
selection_toolbar_policy(),
SelectionToolbarPolicy::Framework
);
assert_eq!(
SelectionToolbarPolicy::default(),
SelectionToolbarPolicy::Framework,
"the default route is the framework-drawn one"
);
set_selection_toolbar_policy(SelectionToolbarPolicy::Native);
assert_eq!(selection_toolbar_policy(), SelectionToolbarPolicy::Native);
set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
}
fn unlock_selection_toolbar_policy_for_test() {
POLICY.lock().unwrap_or_else(|e| e.into_inner()).locked = false;
}
#[test]
fn set_policy_is_unaffected_when_nothing_has_locked_it() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
set_selection_toolbar_policy(SelectionToolbarPolicy::Native);
assert_eq!(selection_toolbar_policy(), SelectionToolbarPolicy::Native);
set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
assert_eq!(
selection_toolbar_policy(),
SelectionToolbarPolicy::Framework
);
}
#[test]
fn locking_the_policy_sets_the_declared_value() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
assert!(
lock_selection_toolbar_policy(SelectionToolbarPolicy::Native),
"the first claim always takes the lock"
);
assert_eq!(
selection_toolbar_policy(),
SelectionToolbarPolicy::Native,
"claiming the lock sets the value it declares"
);
unlock_selection_toolbar_policy_for_test();
}
#[test]
fn a_locked_policy_refuses_a_later_set_and_a_later_claim() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
assert!(lock_selection_toolbar_policy(
SelectionToolbarPolicy::Native
));
set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
assert_eq!(
selection_toolbar_policy(),
SelectionToolbarPolicy::Native,
"a locked slot must not move for an app's later set_selection_toolbar_policy call"
);
assert!(
!lock_selection_toolbar_policy(SelectionToolbarPolicy::Framework),
"a second lock claim must not displace the first"
);
assert_eq!(selection_toolbar_policy(), SelectionToolbarPolicy::Native);
unlock_selection_toolbar_policy_for_test();
set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
}
#[test]
fn install_if_unset_yields_to_an_installed_builder_but_set_replaces_it() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
*BUILDER.lock().unwrap_or_else(|e| e.into_inner()) = None;
assert!(
selection_toolbar_builder().is_none(),
"nothing is installed at rest"
);
let first: SelectionToolbarBuilder = Arc::new(|_req, _size| crate::view::any(FirstView));
assert!(
install_selection_toolbar_builder_if_unset(Arc::clone(&first)),
"the first cooperative install takes the empty slot"
);
let second: SelectionToolbarBuilder = Arc::new(|_req, _size| crate::view::any(SecondView));
assert!(
!install_selection_toolbar_builder_if_unset(Arc::clone(&second)),
"a second cooperative install must not displace the first"
);
assert!(
Arc::ptr_eq(
&selection_toolbar_builder().expect("a builder is installed"),
&first
),
"the slot still holds the first builder"
);
set_selection_toolbar_builder(Arc::clone(&second));
assert!(
Arc::ptr_eq(
&selection_toolbar_builder().expect("a builder is installed"),
&second
),
"an explicit set replaces whatever stood"
);
*BUILDER.lock().unwrap_or_else(|e| e.into_inner()) = None;
}
#[test]
fn publish_is_pass_scoped_and_last_writer_wins() {
let pass = SelectionToolbarPass::enter();
assert_eq!(pass.take(), None, "a fresh pass starts empty");
publish(toolbar_request());
let mut second = toolbar_request();
second.actions.paste = true;
publish(second);
assert_eq!(
pass.take(),
Some(second),
"the last publish of the pass is the one resolved"
);
assert_eq!(pass.take(), None, "and the drain empties the slot");
drop(pass);
publish(toolbar_request());
let next = SelectionToolbarPass::enter();
assert_eq!(
next.take(),
None,
"a stray publish does not survive into the next pass"
);
}
struct FirstView;
struct SecondView;
macro_rules! stub_view {
($name:ident) => {
impl crate::view::View<()> for $name {
type Element = StubWidget;
fn build(&self, _ctx: &mut crate::view::BuildCtx<'_>) -> StubWidget {
StubWidget
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut StubWidget,
_ctx: &mut crate::view::BuildCtx<'_>,
) -> crate::view::ChangeFlags {
crate::view::ChangeFlags::NONE
}
}
};
}
stub_view!(FirstView);
stub_view!(SecondView);
struct StubWidget;
impl crate::widget::Widget for StubWidget {
fn layout(
&mut self,
_ctx: &mut crate::widget::LayoutCtx,
_bc: &crate::layout::BoxConstraints,
) -> Size {
Size::ZERO
}
fn paint(
&mut self,
_ctx: &mut crate::widget::PaintCtx,
_scene: &mut dyn crate::widget::PaintScene,
) {
}
}
}