995 lines
33 KiB
Rust
995 lines
33 KiB
Rust
//! Root overlay adapter. Keep the wrapper mounted while closed so the release
|
||
//! from a dismissal gesture cannot reach the underlying content. Parent modal
|
||
//! ownership is supplied by the host (pass no chooser while a modal is active).
|
||
use super::{Action, Close, Event as ChoiceEvent, MAX_ROWS, Stamp, State as Chooser};
|
||
use crate::style;
|
||
use iced::{
|
||
Border, Color, Element, Event, Font, Length, Point, Rectangle, Size, Theme, Vector,
|
||
advanced::{
|
||
Clipboard, Layout, Shell, Widget, layout, overlay, renderer,
|
||
text::{self, Paragraph as _},
|
||
widget::{Operation, Tree, tree},
|
||
},
|
||
alignment, keyboard, mouse, touch,
|
||
widget::text_input,
|
||
window,
|
||
};
|
||
use std::{cell::Cell, rc::Rc};
|
||
|
||
const ROW: f32 = 34.;
|
||
const MARGIN: f32 = 8.;
|
||
|
||
#[derive(Debug, Clone, Copy)]
|
||
pub(super) struct Geometry {
|
||
pub popup: Rectangle,
|
||
pub search: Rectangle,
|
||
pub current: Rectangle,
|
||
pub detail: Rectangle,
|
||
pub close: Rectangle,
|
||
pub previous: Rectangle,
|
||
pub next: Rectangle,
|
||
pub clear: Rectangle,
|
||
pub rows: usize,
|
||
}
|
||
impl Geometry {
|
||
fn new(anchor: Rectangle, viewport: Size) -> Self {
|
||
let width = anchor
|
||
.width
|
||
.clamp(278., 420.)
|
||
.min((viewport.width - 2. * MARGIN).max(0.));
|
||
let height = 360f32.min((viewport.height - 2. * MARGIN).max(0.));
|
||
let x = anchor.x.clamp(
|
||
MARGIN.min(viewport.width / 2.),
|
||
(viewport.width - MARGIN - width).max(MARGIN.min(viewport.width / 2.)),
|
||
);
|
||
let below = anchor.y + anchor.height + 4.;
|
||
let preferred = if below + height + MARGIN <= viewport.height {
|
||
below
|
||
} else {
|
||
anchor.y - height - 4.
|
||
};
|
||
let y = preferred.clamp(
|
||
MARGIN.min(viewport.height / 2.),
|
||
(viewport.height - MARGIN - height).max(MARGIN.min(viewport.height / 2.)),
|
||
);
|
||
Self::from_bounds(Rectangle {
|
||
x,
|
||
y,
|
||
width,
|
||
height,
|
||
})
|
||
}
|
||
pub(super) fn from_bounds(popup: Rectangle) -> Self {
|
||
let inner = (popup.width - 16.).max(0.);
|
||
let rect = |left, top, width, height| Rectangle {
|
||
x: popup.x + left,
|
||
y: popup.y + top,
|
||
width,
|
||
height,
|
||
};
|
||
let footer = (popup.height - 32.).max(0.);
|
||
let detail_height = if popup.height >= 180. { 32. } else { 0. };
|
||
let detail_top = (footer - detail_height - 4.).max(84.);
|
||
let rows = (((detail_top - 88.).max(0.) / ROW) as usize).min(MAX_ROWS);
|
||
Self {
|
||
popup,
|
||
search: rect(8., 28., inner, 30.),
|
||
current: rect(8., 62., inner, 18.),
|
||
detail: rect(8., detail_top, inner, detail_height),
|
||
close: rect((popup.width - 58.).max(0.), 4., 50f32.min(popup.width), 22.),
|
||
previous: rect(8., footer, 28., 24.),
|
||
next: rect(40., footer, 28., 24.),
|
||
clear: rect((popup.width - 84.).max(0.), footer, 76., 24.),
|
||
rows,
|
||
}
|
||
}
|
||
pub fn row(&self, index: usize) -> Rectangle {
|
||
Rectangle {
|
||
x: self.popup.x + 8.,
|
||
y: self.popup.y + 84. + index as f32 * ROW,
|
||
width: (self.popup.width - 16.).max(0.),
|
||
height: ROW,
|
||
}
|
||
}
|
||
fn hit(&self, chooser: &Chooser, point: Point) -> Option<Action> {
|
||
if self.close.contains(point) {
|
||
return Some(Action::Close(Close::Escape));
|
||
}
|
||
if self.previous.contains(point) && chooser.start() > 0 {
|
||
return Some(Action::Page {
|
||
delta: -1,
|
||
rows: self.rows,
|
||
});
|
||
}
|
||
if self.next.contains(point) && chooser.start() + self.rows < chooser.count() {
|
||
return Some(Action::Page {
|
||
delta: 1,
|
||
rows: self.rows,
|
||
});
|
||
}
|
||
if self.clear.contains(point) && chooser.optional() {
|
||
return Some(Action::Clear);
|
||
}
|
||
chooser
|
||
.visible(self.rows)
|
||
.enumerate()
|
||
.find_map(|(i, c)| self.row(i).contains(point).then_some(Action::Choose(c.id)))
|
||
}
|
||
}
|
||
|
||
#[derive(Default)]
|
||
struct Interaction {
|
||
stamp: Option<Stamp>,
|
||
serial: u64,
|
||
pressed: Option<(ChoiceEvent, Pointer)>,
|
||
closing: bool,
|
||
swallow_mouse: Option<mouse::Button>,
|
||
swallow_touch: Option<touch::Finger>,
|
||
held_mouse: Option<mouse::Button>,
|
||
held_touch: Option<touch::Finger>,
|
||
detail_scroll: f32,
|
||
}
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
enum Pointer {
|
||
Mouse,
|
||
Touch(touch::Finger),
|
||
}
|
||
impl Interaction {
|
||
fn swallow(&mut self, event: &Event) -> bool {
|
||
match event {
|
||
Event::Mouse(mouse::Event::ButtonReleased(button))
|
||
if self.swallow_mouse == Some(*button) =>
|
||
{
|
||
self.swallow_mouse = None;
|
||
true
|
||
}
|
||
Event::Touch(
|
||
touch::Event::FingerLifted { id, .. } | touch::Event::FingerLost { id, .. },
|
||
) if self.swallow_touch == Some(*id) => {
|
||
self.swallow_touch = None;
|
||
true
|
||
}
|
||
_ => false,
|
||
}
|
||
}
|
||
}
|
||
|
||
pub struct Popup<'a, Message, R = iced::Renderer>
|
||
where
|
||
R: text::Renderer<Font = Font>,
|
||
{
|
||
content: Element<'a, Message, Theme, R>,
|
||
input: Element<'a, String, Theme, R>,
|
||
chooser: Option<&'a Chooser>,
|
||
anchor: Rectangle,
|
||
on_event: Rc<dyn Fn(ChoiceEvent) -> Message + 'a>,
|
||
focus_scope: Option<super::trigger::FormScope>,
|
||
}
|
||
impl<'a, Message: Clone + 'a, R: text::Renderer<Font = Font> + 'a> Popup<'a, Message, R> {
|
||
pub fn new(
|
||
content: impl Into<Element<'a, Message, Theme, R>>,
|
||
chooser: Option<&'a Chooser>,
|
||
anchor: Rectangle,
|
||
on_event: impl Fn(ChoiceEvent) -> Message + 'a,
|
||
) -> Self {
|
||
let chooser = chooser.filter(|state| state.is_open());
|
||
let input = text_input(
|
||
"Name, @alias, kind or containment",
|
||
chooser.map_or("", Chooser::query),
|
||
)
|
||
.on_input(std::convert::identity)
|
||
.size(12)
|
||
.padding(6)
|
||
.width(Length::Fill)
|
||
.into();
|
||
Self {
|
||
content: content.into(),
|
||
input,
|
||
chooser,
|
||
anchor,
|
||
on_event: Rc::new(on_event),
|
||
focus_scope: None,
|
||
}
|
||
}
|
||
pub fn focus_scope(mut self, scope: Option<super::trigger::FormScope>) -> Self {
|
||
self.focus_scope = scope;
|
||
self
|
||
}
|
||
fn reconcile(&self, tree: &mut Tree) {
|
||
let state = tree.state.downcast_mut::<Interaction>();
|
||
let stamp = self.chooser.map(Chooser::stamp);
|
||
if state.stamp.as_ref().map(|s| s.invocation) != stamp.as_ref().map(|s| s.invocation) {
|
||
let input = tree.children[1]
|
||
.state
|
||
.downcast_mut::<text_input::State<R::Paragraph>>();
|
||
if stamp.is_some() {
|
||
input.focus();
|
||
} else {
|
||
input.unfocus();
|
||
}
|
||
state.closing = false;
|
||
state.serial = self.chooser.map_or(0, Chooser::query_serial);
|
||
}
|
||
if state.stamp != stamp {
|
||
state.pressed = None;
|
||
// A queued Choose can be rejected after a preceding query changed
|
||
// the result generation. Reconciliation must unlock this view.
|
||
state.closing = false;
|
||
state.detail_scroll = 0.;
|
||
}
|
||
state.stamp = stamp;
|
||
}
|
||
}
|
||
impl<Message: Clone, R: text::Renderer<Font = Font>> Widget<Message, Theme, R>
|
||
for Popup<'_, Message, R>
|
||
{
|
||
fn tag(&self) -> tree::Tag {
|
||
tree::Tag::of::<Interaction>()
|
||
}
|
||
fn state(&self) -> tree::State {
|
||
tree::State::new(Interaction::default())
|
||
}
|
||
fn children(&self) -> Vec<Tree> {
|
||
vec![
|
||
Tree::new(self.content.as_widget()),
|
||
Tree::new(self.input.as_widget()),
|
||
]
|
||
}
|
||
fn diff(&self, tree: &mut Tree) {
|
||
tree.children[0].diff(self.content.as_widget());
|
||
tree.children[1].diff(self.input.as_widget());
|
||
self.reconcile(tree);
|
||
}
|
||
fn size(&self) -> Size<Length> {
|
||
self.content.as_widget().size()
|
||
}
|
||
fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node {
|
||
self.reconcile(tree);
|
||
self.content
|
||
.as_widget_mut()
|
||
.layout(&mut tree.children[0], renderer, limits)
|
||
}
|
||
fn operate(
|
||
&mut self,
|
||
tree: &mut Tree,
|
||
layout: Layout<'_>,
|
||
renderer: &R,
|
||
operation: &mut dyn Operation,
|
||
) {
|
||
let mut owner =
|
||
super::trigger::FormOwner(self.focus_scope.filter(|_| self.chooser.is_none()));
|
||
operation.custom(None, layout.bounds(), &mut owner);
|
||
self.content
|
||
.as_widget_mut()
|
||
.operate(&mut tree.children[0], layout, renderer, operation);
|
||
}
|
||
fn update(
|
||
&mut self,
|
||
tree: &mut Tree,
|
||
event: &Event,
|
||
layout: Layout<'_>,
|
||
cursor: mouse::Cursor,
|
||
renderer: &R,
|
||
clipboard: &mut dyn Clipboard,
|
||
shell: &mut Shell<'_, Message>,
|
||
viewport: &Rectangle,
|
||
) {
|
||
let state = tree.state.downcast_mut::<Interaction>();
|
||
if state.swallow(event) || self.chooser.is_some() && is_input(event) {
|
||
shell.capture_event();
|
||
return;
|
||
}
|
||
if is_input(event) && self.focus_scope.is_some_and(|scope| scope.modal) {
|
||
super::trigger::own_input(&mut self.content, &mut tree.children[0], layout, renderer);
|
||
}
|
||
if let Event::Keyboard(keyboard::Event::KeyPressed {
|
||
key: keyboard::Key::Named(keyboard::key::Named::Tab),
|
||
modifiers,
|
||
..
|
||
}) = event
|
||
&& let Some(scope) = self.focus_scope
|
||
&& super::trigger::tab(
|
||
&mut self.content,
|
||
&mut tree.children[0],
|
||
layout,
|
||
renderer,
|
||
scope,
|
||
modifiers.shift(),
|
||
)
|
||
{
|
||
shell.capture_event();
|
||
shell.request_redraw();
|
||
return;
|
||
}
|
||
super::trigger::measure(
|
||
&mut self.content,
|
||
&mut tree.children[0],
|
||
layout,
|
||
renderer,
|
||
*viewport,
|
||
);
|
||
self.content.as_widget_mut().update(
|
||
&mut tree.children[0],
|
||
event,
|
||
layout,
|
||
cursor,
|
||
renderer,
|
||
clipboard,
|
||
shell,
|
||
viewport,
|
||
);
|
||
}
|
||
fn draw(
|
||
&self,
|
||
tree: &Tree,
|
||
renderer: &mut R,
|
||
theme: &Theme,
|
||
style: &renderer::Style,
|
||
layout: Layout<'_>,
|
||
cursor: mouse::Cursor,
|
||
viewport: &Rectangle,
|
||
) {
|
||
self.content.as_widget().draw(
|
||
&tree.children[0],
|
||
renderer,
|
||
theme,
|
||
style,
|
||
layout,
|
||
if self.chooser.is_some() {
|
||
mouse::Cursor::Unavailable
|
||
} else {
|
||
cursor
|
||
},
|
||
viewport,
|
||
);
|
||
}
|
||
fn mouse_interaction(
|
||
&self,
|
||
tree: &Tree,
|
||
layout: Layout<'_>,
|
||
cursor: mouse::Cursor,
|
||
viewport: &Rectangle,
|
||
renderer: &R,
|
||
) -> mouse::Interaction {
|
||
if self.chooser.is_some() {
|
||
mouse::Interaction::None
|
||
} else {
|
||
self.content.as_widget().mouse_interaction(
|
||
&tree.children[0],
|
||
layout,
|
||
cursor,
|
||
viewport,
|
||
renderer,
|
||
)
|
||
}
|
||
}
|
||
fn overlay<'b>(
|
||
&'b mut self,
|
||
tree: &'b mut Tree,
|
||
layout: Layout<'b>,
|
||
renderer: &R,
|
||
viewport: &Rectangle,
|
||
translation: Vector,
|
||
) -> Option<overlay::Element<'b, Message, Theme, R>> {
|
||
if let Some(chooser) = self.chooser {
|
||
Some(overlay::Element::new(Box::new(ChooserOverlay {
|
||
chooser,
|
||
anchor: self.anchor,
|
||
state: tree.state.downcast_mut(),
|
||
input: &mut self.input,
|
||
input_tree: &mut tree.children[1],
|
||
on_event: &*self.on_event,
|
||
})))
|
||
} else {
|
||
self.content.as_widget_mut().overlay(
|
||
&mut tree.children[0],
|
||
layout,
|
||
renderer,
|
||
viewport,
|
||
translation,
|
||
)
|
||
}
|
||
}
|
||
}
|
||
impl<'a, Message: Clone + 'a, R: text::Renderer<Font = Font> + 'a> From<Popup<'a, Message, R>>
|
||
for Element<'a, Message, Theme, R>
|
||
{
|
||
fn from(popup: Popup<'a, Message, R>) -> Self {
|
||
Element::new(popup)
|
||
}
|
||
}
|
||
|
||
struct ChooserOverlay<'b, 'a, Message, R: text::Renderer<Font = Font>> {
|
||
chooser: &'a Chooser,
|
||
anchor: Rectangle,
|
||
state: &'b mut Interaction,
|
||
input: &'b mut Element<'a, String, Theme, R>,
|
||
input_tree: &'b mut Tree,
|
||
on_event: &'b dyn Fn(ChoiceEvent) -> Message,
|
||
}
|
||
impl<Message, R: text::Renderer<Font = Font>> ChooserOverlay<'_, '_, Message, R> {
|
||
fn geometry(&self, layout: Layout<'_>) -> Geometry {
|
||
Geometry::from_bounds(
|
||
layout
|
||
.children()
|
||
.next()
|
||
.expect("measured chooser bounds")
|
||
.bounds(),
|
||
)
|
||
}
|
||
fn publish(&mut self, action: Action, shell: &mut Shell<'_, Message>) {
|
||
if self.state.closing {
|
||
return;
|
||
}
|
||
if action.writes_staged_value() || matches!(action, Action::Close(_)) {
|
||
self.state.closing = true;
|
||
self.state.swallow_mouse = self.state.held_mouse.or(self.state.swallow_mouse);
|
||
self.state.swallow_touch = self.state.held_touch.or(self.state.swallow_touch);
|
||
}
|
||
shell.publish((self.on_event)(self.chooser.event(action)));
|
||
}
|
||
fn press(
|
||
&mut self,
|
||
point: Point,
|
||
pointer: Pointer,
|
||
geometry: Geometry,
|
||
shell: &mut Shell<'_, Message>,
|
||
) {
|
||
if !geometry.popup.contains(point) {
|
||
self.publish(Action::Close(Close::Outside), shell);
|
||
} else {
|
||
self.state.pressed = geometry
|
||
.hit(self.chooser, point)
|
||
.map(|action| (self.chooser.event(action), pointer));
|
||
}
|
||
}
|
||
fn release(
|
||
&mut self,
|
||
point: Point,
|
||
pointer: Pointer,
|
||
geometry: Geometry,
|
||
shell: &mut Shell<'_, Message>,
|
||
) {
|
||
if let Some((pressed, origin)) = self.state.pressed.take()
|
||
&& origin == pointer
|
||
&& pressed.stamp == self.chooser.stamp()
|
||
&& geometry.hit(self.chooser, point).as_ref() == Some(&pressed.action)
|
||
{
|
||
self.publish(pressed.action, shell);
|
||
}
|
||
}
|
||
}
|
||
impl<Message: Clone, R: text::Renderer<Font = Font>> overlay::Overlay<Message, Theme, R>
|
||
for ChooserOverlay<'_, '_, Message, R>
|
||
{
|
||
fn layout(&mut self, renderer: &R, bounds: Size) -> layout::Node {
|
||
let geometry = Geometry::new(self.anchor, bounds);
|
||
let input = self
|
||
.input
|
||
.as_widget_mut()
|
||
.layout(
|
||
self.input_tree,
|
||
renderer,
|
||
&layout::Limits::new(Size::ZERO, geometry.search.size()),
|
||
)
|
||
.move_to(geometry.search.position());
|
||
layout::Node::with_children(
|
||
bounds,
|
||
vec![
|
||
layout::Node::new(geometry.popup.size()).move_to(geometry.popup.position()),
|
||
input,
|
||
],
|
||
)
|
||
}
|
||
fn operate(&mut self, layout: Layout<'_>, renderer: &R, operation: &mut dyn Operation) {
|
||
self.input.as_widget_mut().operate(
|
||
self.input_tree,
|
||
layout.children().nth(1).unwrap(),
|
||
renderer,
|
||
operation,
|
||
);
|
||
}
|
||
fn update(
|
||
&mut self,
|
||
event: &Event,
|
||
layout: Layout<'_>,
|
||
cursor: mouse::Cursor,
|
||
renderer: &R,
|
||
clipboard: &mut dyn Clipboard,
|
||
shell: &mut Shell<'_, Message>,
|
||
) {
|
||
let geometry = self.geometry(layout);
|
||
match event {
|
||
Event::Mouse(mouse::Event::ButtonPressed(button)) => {
|
||
self.state.held_mouse = Some(*button)
|
||
}
|
||
Event::Mouse(mouse::Event::ButtonReleased(button))
|
||
if self.state.held_mouse == Some(*button) =>
|
||
{
|
||
self.state.held_mouse = None
|
||
}
|
||
Event::Touch(touch::Event::FingerPressed { id, .. }) => {
|
||
self.state.held_touch = Some(*id)
|
||
}
|
||
Event::Touch(
|
||
touch::Event::FingerLifted { id, .. } | touch::Event::FingerLost { id, .. },
|
||
) if self.state.held_touch == Some(*id) => self.state.held_touch = None,
|
||
_ => {}
|
||
}
|
||
if self.state.swallow(event) {
|
||
shell.capture_event();
|
||
return;
|
||
}
|
||
if self.state.closing {
|
||
if is_input(event) {
|
||
shell.capture_event();
|
||
}
|
||
return;
|
||
}
|
||
let mut handled = false;
|
||
match event {
|
||
Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => {
|
||
use keyboard::key::Named;
|
||
let action = match key {
|
||
keyboard::Key::Named(Named::Escape) => Some(Action::Close(Close::Escape)),
|
||
keyboard::Key::Named(Named::Tab) => Some(Action::Close(Close::Tab {
|
||
backwards: modifiers.shift(),
|
||
})),
|
||
keyboard::Key::Named(Named::ArrowDown) => Some(Action::Move {
|
||
delta: 1,
|
||
rows: geometry.rows,
|
||
}),
|
||
keyboard::Key::Named(Named::ArrowUp) => Some(Action::Move {
|
||
delta: -1,
|
||
rows: geometry.rows,
|
||
}),
|
||
keyboard::Key::Named(Named::PageDown) => Some(Action::Page {
|
||
delta: 1,
|
||
rows: geometry.rows,
|
||
}),
|
||
keyboard::Key::Named(Named::PageUp) => Some(Action::Page {
|
||
delta: -1,
|
||
rows: geometry.rows,
|
||
}),
|
||
keyboard::Key::Named(Named::Enter) => {
|
||
handled = true;
|
||
self.chooser
|
||
.highlighted()
|
||
.filter(|c| self.chooser.visible(geometry.rows).any(|v| v.id == c.id))
|
||
.map(|c| Action::Choose(c.id))
|
||
}
|
||
keyboard::Key::Character(c)
|
||
if modifiers.control()
|
||
&& modifiers.shift()
|
||
&& c.eq_ignore_ascii_case("c") =>
|
||
{
|
||
clipboard.write(
|
||
iced::advanced::clipboard::Kind::Standard,
|
||
self.chooser
|
||
.highlighted()
|
||
.map_or_else(|| self.chooser.current.label(), |c| c.detail()),
|
||
);
|
||
handled = true;
|
||
None
|
||
}
|
||
_ => None,
|
||
};
|
||
if let Some(action) = action {
|
||
self.publish(action, shell);
|
||
handled = true;
|
||
}
|
||
}
|
||
Event::Window(window::Event::Resized(_) | window::Event::Unfocused) => {
|
||
self.publish(Action::Close(Close::WindowChanged), shell);
|
||
handled = true;
|
||
}
|
||
Event::Mouse(mouse::Event::ButtonPressed(button)) => {
|
||
if let Some(point) = cursor.position() {
|
||
if !geometry.popup.contains(point) {
|
||
self.state.swallow_mouse = Some(*button);
|
||
self.press(point, Pointer::Mouse, geometry, shell);
|
||
handled = true;
|
||
} else if *button == mouse::Button::Left && !geometry.search.contains(point) {
|
||
self.press(point, Pointer::Mouse, geometry, shell);
|
||
handled = true;
|
||
}
|
||
}
|
||
}
|
||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||
if self.state.pressed.is_some() =>
|
||
{
|
||
if let Some(point) = cursor.position() {
|
||
self.release(point, Pointer::Mouse, geometry, shell);
|
||
} else {
|
||
self.state.pressed = None;
|
||
}
|
||
handled = true;
|
||
}
|
||
Event::Mouse(mouse::Event::CursorMoved { position })
|
||
if self.state.pressed.is_none() =>
|
||
{
|
||
if let Some(Action::Choose(id)) = geometry.hit(self.chooser, *position)
|
||
&& self
|
||
.chooser
|
||
.highlighted()
|
||
.is_none_or(|candidate| candidate.id != id)
|
||
{
|
||
self.publish(Action::Highlight(id), shell);
|
||
}
|
||
}
|
||
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
|
||
let (x, y) = match delta {
|
||
mouse::ScrollDelta::Lines { x, y } => (*x * 24., *y * 24.),
|
||
mouse::ScrollDelta::Pixels { x, y } => (*x, *y),
|
||
};
|
||
self.state.pressed = None;
|
||
if cursor
|
||
.position()
|
||
.is_some_and(|p| geometry.detail.contains(p))
|
||
{
|
||
let detail = self
|
||
.chooser
|
||
.highlighted()
|
||
.map_or_else(|| self.chooser.current.label(), |c| c.detail());
|
||
let max = (measure::<R>(&detail, 11., renderer.default_font())
|
||
- geometry.detail.width)
|
||
.max(0.);
|
||
self.state.detail_scroll =
|
||
(self.state.detail_scroll - if x != 0. { x } else { y }).clamp(0., max);
|
||
shell.request_redraw();
|
||
} else if y != 0. {
|
||
self.publish(
|
||
Action::Page {
|
||
delta: if y < 0. { 1 } else { -1 },
|
||
rows: geometry.rows,
|
||
},
|
||
shell,
|
||
);
|
||
}
|
||
handled = true;
|
||
}
|
||
Event::Touch(touch::Event::FingerPressed { id, position })
|
||
if !geometry.search.contains(*position) =>
|
||
{
|
||
if !geometry.popup.contains(*position) {
|
||
self.state.swallow_touch = Some(*id);
|
||
}
|
||
self.press(*position, Pointer::Touch(*id), geometry, shell);
|
||
handled = true;
|
||
}
|
||
Event::Touch(touch::Event::FingerLifted { id, position })
|
||
if self.state.pressed.is_some() =>
|
||
{
|
||
self.release(*position, Pointer::Touch(*id), geometry, shell);
|
||
handled = true;
|
||
}
|
||
Event::Touch(touch::Event::FingerLost { .. } | touch::Event::FingerMoved { .. }) => {
|
||
self.state.pressed = None;
|
||
}
|
||
_ => {}
|
||
}
|
||
if !handled {
|
||
let mut messages = Vec::new();
|
||
let mut input_shell = Shell::new(&mut messages);
|
||
self.input.as_widget_mut().update(
|
||
self.input_tree,
|
||
event,
|
||
layout.children().nth(1).unwrap(),
|
||
cursor,
|
||
renderer,
|
||
clipboard,
|
||
&mut input_shell,
|
||
&geometry.popup,
|
||
);
|
||
let serial = Cell::new(self.state.serial);
|
||
shell.merge(input_shell, |text| {
|
||
serial.set(serial.get().saturating_add(1));
|
||
(self.on_event)(self.chooser.event(Action::Query {
|
||
text,
|
||
serial: serial.get(),
|
||
}))
|
||
});
|
||
self.state.serial = serial.get();
|
||
}
|
||
if is_input(event) {
|
||
shell.capture_event();
|
||
}
|
||
}
|
||
fn draw(
|
||
&self,
|
||
renderer: &mut R,
|
||
theme: &Theme,
|
||
renderer_style: &renderer::Style,
|
||
layout: Layout<'_>,
|
||
cursor: mouse::Cursor,
|
||
) {
|
||
let geometry = self.geometry(layout);
|
||
renderer.fill_quad(
|
||
renderer::Quad {
|
||
bounds: geometry.popup,
|
||
border: Border {
|
||
color: style::LINE,
|
||
width: 1.,
|
||
radius: 8.into(),
|
||
},
|
||
..Default::default()
|
||
},
|
||
Color::WHITE,
|
||
);
|
||
renderer.with_layer(geometry.popup, |renderer| {
|
||
label(
|
||
renderer,
|
||
&self.chooser.scope_label,
|
||
11.,
|
||
style::MUTED,
|
||
Rectangle {
|
||
x: geometry.popup.x + 8.,
|
||
y: geometry.popup.y + 4.,
|
||
width: (geometry.popup.width - 78.).max(0.),
|
||
height: 22.,
|
||
},
|
||
);
|
||
label(renderer, "Close", 11., style::TEAL, geometry.close);
|
||
label(
|
||
renderer,
|
||
&self.chooser.current.label(),
|
||
11.,
|
||
style::MUTED,
|
||
geometry.current,
|
||
);
|
||
for (i, candidate) in self.chooser.visible(geometry.rows).enumerate() {
|
||
let row = geometry.row(i);
|
||
if self
|
||
.chooser
|
||
.highlighted()
|
||
.is_some_and(|c| c.id == candidate.id)
|
||
{
|
||
renderer.fill_quad(
|
||
renderer::Quad {
|
||
bounds: row,
|
||
..Default::default()
|
||
},
|
||
Color::from_rgb8(218, 238, 235),
|
||
);
|
||
}
|
||
let alias_width = (row.width * 0.42).min(
|
||
measure::<R>(
|
||
&format!("@{}", candidate.alias),
|
||
11.,
|
||
renderer.default_font(),
|
||
) + 4.,
|
||
);
|
||
label(
|
||
renderer,
|
||
&candidate.name,
|
||
12.,
|
||
style::INK,
|
||
Rectangle {
|
||
width: (row.width - alias_width - 6.).max(0.),
|
||
height: 17.,
|
||
..row
|
||
},
|
||
);
|
||
label(
|
||
renderer,
|
||
&format!("@{}", candidate.alias),
|
||
11.,
|
||
style::INK,
|
||
Rectangle {
|
||
x: row.x + row.width - alias_width,
|
||
width: alias_width,
|
||
height: 17.,
|
||
..row
|
||
},
|
||
);
|
||
label(
|
||
renderer,
|
||
&candidate.context_label(),
|
||
10.,
|
||
style::MUTED,
|
||
Rectangle {
|
||
y: row.y + 17.,
|
||
height: 17.,
|
||
..row
|
||
},
|
||
);
|
||
}
|
||
if self.chooser.count() == 0 {
|
||
label(
|
||
renderer,
|
||
"No compatible matches. Clear the query.",
|
||
11.,
|
||
style::MUTED,
|
||
geometry.row(0),
|
||
);
|
||
}
|
||
let detail = self
|
||
.chooser
|
||
.highlighted()
|
||
.map_or_else(|| self.chooser.current.label(), |c| c.detail());
|
||
renderer.with_layer(geometry.detail, |renderer| {
|
||
raw_label(
|
||
renderer,
|
||
detail,
|
||
11.,
|
||
style::MUTED,
|
||
Rectangle {
|
||
x: geometry.detail.x - self.state.detail_scroll,
|
||
width: geometry.detail.width + self.state.detail_scroll,
|
||
height: 16.,
|
||
..geometry.detail
|
||
},
|
||
);
|
||
label(
|
||
renderer,
|
||
"Scroll details · Ctrl+Shift+C copies",
|
||
10.,
|
||
style::MUTED,
|
||
Rectangle {
|
||
y: geometry.detail.y + 16.,
|
||
height: 16.,
|
||
..geometry.detail
|
||
},
|
||
);
|
||
});
|
||
label(
|
||
renderer,
|
||
"‹",
|
||
15.,
|
||
if self.chooser.start() > 0 {
|
||
style::TEAL
|
||
} else {
|
||
style::MUTED
|
||
},
|
||
geometry.previous,
|
||
);
|
||
label(
|
||
renderer,
|
||
"›",
|
||
15.,
|
||
if self.chooser.start() + geometry.rows < self.chooser.count() {
|
||
style::TEAL
|
||
} else {
|
||
style::MUTED
|
||
},
|
||
geometry.next,
|
||
);
|
||
let count = if self.chooser.count() == 0 {
|
||
"0 results".into()
|
||
} else {
|
||
format!(
|
||
"{}–{} / {}",
|
||
self.chooser.start() + 1,
|
||
(self.chooser.start() + geometry.rows).min(self.chooser.count()),
|
||
self.chooser.count()
|
||
)
|
||
};
|
||
label(
|
||
renderer,
|
||
&count,
|
||
11.,
|
||
style::MUTED,
|
||
Rectangle {
|
||
x: geometry.next.x + 30.,
|
||
width: (geometry.clear.x - geometry.next.x - 32.).max(0.),
|
||
..geometry.next
|
||
},
|
||
);
|
||
if self.chooser.optional() {
|
||
label(
|
||
renderer,
|
||
"Clear reference",
|
||
10.,
|
||
style::TEAL,
|
||
geometry.clear,
|
||
);
|
||
}
|
||
self.input.as_widget().draw(
|
||
self.input_tree,
|
||
renderer,
|
||
theme,
|
||
renderer_style,
|
||
layout.children().nth(1).unwrap(),
|
||
cursor,
|
||
&geometry.popup,
|
||
);
|
||
});
|
||
}
|
||
fn mouse_interaction(
|
||
&self,
|
||
layout: Layout<'_>,
|
||
cursor: mouse::Cursor,
|
||
renderer: &R,
|
||
) -> mouse::Interaction {
|
||
let geometry = self.geometry(layout);
|
||
if cursor
|
||
.position()
|
||
.is_some_and(|p| geometry.hit(self.chooser, p).is_some())
|
||
{
|
||
mouse::Interaction::Pointer
|
||
} else {
|
||
self.input.as_widget().mouse_interaction(
|
||
self.input_tree,
|
||
layout.children().nth(1).unwrap(),
|
||
cursor,
|
||
&geometry.popup,
|
||
renderer,
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
fn is_input(event: &Event) -> bool {
|
||
matches!(
|
||
event,
|
||
Event::Mouse(_) | Event::Touch(_) | Event::Keyboard(_) | Event::InputMethod(_)
|
||
)
|
||
}
|
||
fn measure<R: text::Renderer<Font = Font>>(content: &str, size: f32, font: Font) -> f32 {
|
||
R::Paragraph::with_text(text::Text {
|
||
content,
|
||
bounds: Size::INFINITE,
|
||
size: size.into(),
|
||
line_height: text::LineHeight::default(),
|
||
font,
|
||
align_x: text::Alignment::Left,
|
||
align_y: alignment::Vertical::Center,
|
||
shaping: text::Shaping::Advanced,
|
||
wrapping: text::Wrapping::None,
|
||
})
|
||
.min_width()
|
||
}
|
||
fn label<R: text::Renderer<Font = Font>>(
|
||
renderer: &mut R,
|
||
content: &str,
|
||
size: f32,
|
||
color: Color,
|
||
bounds: Rectangle,
|
||
) {
|
||
let font = renderer.default_font();
|
||
let mut display = content.to_owned();
|
||
if measure::<R>(content, size, font) > bounds.width {
|
||
let ends: Vec<_> = content
|
||
.char_indices()
|
||
.map(|(i, _)| i)
|
||
.chain(std::iter::once(content.len()))
|
||
.collect();
|
||
let (mut low, mut high) = (0, ends.len());
|
||
while low + 1 < high {
|
||
let middle = (low + high) / 2;
|
||
if measure::<R>(&format!("{}…", &content[..ends[middle]]), size, font) <= bounds.width
|
||
{
|
||
low = middle;
|
||
} else {
|
||
high = middle;
|
||
}
|
||
}
|
||
display = format!("{}…", &content[..ends[low]]);
|
||
}
|
||
raw_label(renderer, display, size, color, bounds);
|
||
}
|
||
fn raw_label<R: text::Renderer<Font = Font>>(
|
||
renderer: &mut R,
|
||
content: String,
|
||
size: f32,
|
||
color: Color,
|
||
bounds: Rectangle,
|
||
) {
|
||
renderer.fill_text(
|
||
text::Text {
|
||
content,
|
||
bounds: bounds.size(),
|
||
size: size.into(),
|
||
line_height: text::LineHeight::default(),
|
||
font: renderer.default_font(),
|
||
align_x: text::Alignment::Left,
|
||
align_y: alignment::Vertical::Center,
|
||
shaping: text::Shaping::Advanced,
|
||
wrapping: text::Wrapping::None,
|
||
},
|
||
Point::new(bounds.x, bounds.center_y()),
|
||
color,
|
||
bounds,
|
||
);
|
||
}
|