wip: on hover switch and fix for nesting
This commit is contained in:
parent
ff41ff0532
commit
51c391a656
3 changed files with 364 additions and 281 deletions
|
|
@ -4,7 +4,7 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["wayland"]
|
default = []
|
||||||
wayland = ["libcosmic/wayland"]
|
wayland = ["libcosmic/wayland"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
@ -25,4 +25,5 @@ features = [
|
||||||
"wgpu",
|
"wgpu",
|
||||||
"single-instance",
|
"single-instance",
|
||||||
"multi-window",
|
"multi-window",
|
||||||
|
"surface-message",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ use super::{
|
||||||
use crate::{
|
use crate::{
|
||||||
Renderer,
|
Renderer,
|
||||||
style::menu_bar::StyleSheet,
|
style::menu_bar::StyleSheet,
|
||||||
surface::action::destroy_popup,
|
|
||||||
widget::{
|
widget::{
|
||||||
RcWrapper,
|
RcWrapper,
|
||||||
dropdown::menu::{self, State},
|
dropdown::menu::{self, State},
|
||||||
|
|
@ -64,8 +63,7 @@ impl MenuBarStateInner {
|
||||||
self.menu_states
|
self.menu_states
|
||||||
.get(index)
|
.get(index)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|v| v.iter())
|
.flat_map(|v| v.iter())
|
||||||
.flatten()
|
|
||||||
.take_while(|ms| ms.index.is_some())
|
.take_while(|ms| ms.index.is_some())
|
||||||
.map(|ms| ms.index.expect("No indices were found in the menu state."))
|
.map(|ms| ms.index.expect("No indices were found in the menu state."))
|
||||||
}
|
}
|
||||||
|
|
@ -356,6 +354,145 @@ where
|
||||||
self.on_surface_action = Some(Arc::new(handler));
|
self.on_surface_action = Some(Arc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_popup(
|
||||||
|
&mut self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
view_cursor: Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
my_state: &mut MenuBarState,
|
||||||
|
) -> event::Status {
|
||||||
|
let mut status = event::Status::Ignored;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
|
||||||
|
// TODO emit Message to open menu
|
||||||
|
if self.window_id != window::Id::NONE && self.on_surface_action.is_some() {
|
||||||
|
use crate::surface::action::destroy_popup;
|
||||||
|
use iced_runtime::platform_specific::wayland::popup::{
|
||||||
|
SctkPopupSettings, SctkPositioner,
|
||||||
|
};
|
||||||
|
|
||||||
|
let surface_action = self.on_surface_action.as_ref().unwrap();
|
||||||
|
let old_active_root = my_state.inner.with_data(|state| {
|
||||||
|
state
|
||||||
|
.active_root
|
||||||
|
.get(0)
|
||||||
|
.filter(|r| r.len() == 1)
|
||||||
|
.map(|r| r[0])
|
||||||
|
});
|
||||||
|
|
||||||
|
// if position is not on menu bar button skip.
|
||||||
|
let position = view_cursor.position();
|
||||||
|
let hovered_root = layout
|
||||||
|
.children()
|
||||||
|
.position(|lo| view_cursor.is_over(lo.bounds()));
|
||||||
|
|
||||||
|
if old_active_root == hovered_root {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
let (id, root_list) = my_state.inner.with_data_mut(|state| {
|
||||||
|
if let Some(id) = state.popup_id.get(&self.window_id).copied() {
|
||||||
|
// close existing popups
|
||||||
|
state.menu_states.clear();
|
||||||
|
state.active_root.clear();
|
||||||
|
shell.publish(surface_action(destroy_popup(id)));
|
||||||
|
state.view_cursor = view_cursor;
|
||||||
|
(id, layout.children().map(|lo| lo.bounds()).collect())
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
window::Id::unique(),
|
||||||
|
layout.children().map(|lo| lo.bounds()).collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut popup_menu: Menu<'static, _> = Menu {
|
||||||
|
tree: my_state.clone(),
|
||||||
|
menu_roots: std::borrow::Cow::Owned(self.menu_roots.clone()),
|
||||||
|
bounds_expand: self.bounds_expand,
|
||||||
|
menu_overlays_parent: false,
|
||||||
|
close_condition: self.close_condition,
|
||||||
|
item_width: self.item_width,
|
||||||
|
item_height: self.item_height,
|
||||||
|
bar_bounds: layout.bounds(),
|
||||||
|
main_offset: self.main_offset,
|
||||||
|
cross_offset: self.cross_offset,
|
||||||
|
root_bounds_list: root_list,
|
||||||
|
path_highlight: self.path_highlight,
|
||||||
|
style: std::borrow::Cow::Owned(self.style.clone()),
|
||||||
|
position: Point::new(0., 0.),
|
||||||
|
is_overlay: false,
|
||||||
|
window_id: id,
|
||||||
|
depth: 0,
|
||||||
|
on_surface_action: self.on_surface_action.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
init_root_menu(
|
||||||
|
&mut popup_menu,
|
||||||
|
renderer,
|
||||||
|
shell,
|
||||||
|
view_cursor.position().unwrap(),
|
||||||
|
viewport.size(),
|
||||||
|
Vector::new(0., 0.),
|
||||||
|
layout.bounds(),
|
||||||
|
self.main_offset as f32,
|
||||||
|
);
|
||||||
|
let anchor_rect = my_state.inner.with_data_mut(|state| {
|
||||||
|
state.popup_id.insert(self.window_id, id);
|
||||||
|
state.menu_states[0]
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.index.is_none())
|
||||||
|
.map(|s| s.menu_bounds.parent_bounds)
|
||||||
|
.map_or_else(
|
||||||
|
|| {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
Rectangle {
|
||||||
|
x: bounds.x as i32,
|
||||||
|
y: bounds.y as i32,
|
||||||
|
width: bounds.width as i32,
|
||||||
|
height: bounds.height as i32,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|r| Rectangle {
|
||||||
|
x: r.x as i32,
|
||||||
|
y: r.y as i32,
|
||||||
|
width: r.width as i32,
|
||||||
|
height: r.height as i32,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let menu_node = popup_menu.layout(renderer, Limits::NONE.min_width(1.).min_height(1.));
|
||||||
|
let popup_size = menu_node.size();
|
||||||
|
let positioner = SctkPositioner {
|
||||||
|
size: Some((popup_size.width.ceil() as u32 + 2, popup_size.height.ceil() as u32 + 2)),
|
||||||
|
anchor_rect,
|
||||||
|
anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft,
|
||||||
|
gravity:cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
|
||||||
|
reactive: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let parent = self.window_id;
|
||||||
|
shell.publish((surface_action)(crate::surface::action::simple_popup(
|
||||||
|
move || SctkPopupSettings {
|
||||||
|
parent,
|
||||||
|
id,
|
||||||
|
positioner: positioner.clone(),
|
||||||
|
parent_size: None,
|
||||||
|
grab: true,
|
||||||
|
close_with_children: false,
|
||||||
|
input_zone: None,
|
||||||
|
},
|
||||||
|
Some(move || {
|
||||||
|
Element::from(crate::widget::container(popup_menu.clone()).center(Length::Fill))
|
||||||
|
.map(crate::action::app)
|
||||||
|
}),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
status
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl<Message> Widget<Message, crate::Theme, Renderer> for MenuBar<Message>
|
impl<Message> Widget<Message, crate::Theme, Renderer> for MenuBar<Message>
|
||||||
where
|
where
|
||||||
|
|
@ -448,7 +585,7 @@ where
|
||||||
.inner
|
.inner
|
||||||
.with_data(|d| !d.open && !d.active_root.is_empty());
|
.with_data(|d| !d.open && !d.active_root.is_empty());
|
||||||
|
|
||||||
my_state.inner.with_data_mut(|state| {
|
let open = my_state.inner.with_data_mut(|state| {
|
||||||
if reset {
|
if reset {
|
||||||
if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() {
|
if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() {
|
||||||
dbg!("reset destroy");
|
dbg!("reset destroy");
|
||||||
|
|
@ -459,6 +596,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
state.open
|
||||||
});
|
});
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
|
|
@ -467,149 +605,48 @@ where
|
||||||
let mut create_popup = false;
|
let mut create_popup = false;
|
||||||
if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) {
|
if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) {
|
||||||
state.view_cursor = view_cursor;
|
state.view_cursor = view_cursor;
|
||||||
dbg!(view_cursor.is_over(layout.bounds()));
|
|
||||||
state.open = true;
|
state.open = true;
|
||||||
create_popup = true;
|
create_popup = true;
|
||||||
} else if let Some(id) = state.popup_id.remove(&self.window_id) {
|
} else if let Some(id) = state.popup_id.remove(&self.window_id) {
|
||||||
dbg!("destroy popup...");
|
|
||||||
state.menu_states.clear();
|
state.menu_states.clear();
|
||||||
state.active_root.clear();
|
state.active_root.clear();
|
||||||
let surface_action = self.on_surface_action.as_ref().unwrap();
|
let surface_action = self.on_surface_action.as_ref().unwrap();
|
||||||
state.open = false;
|
state.open = false;
|
||||||
shell.publish(surface_action(destroy_popup(id)));
|
#[cfg(all(
|
||||||
|
feature = "wayland",
|
||||||
|
feature = "winit",
|
||||||
|
feature = "surface-message"
|
||||||
|
))]
|
||||||
|
shell.publish(surface_action(crate::surface::action::destroy_popup(id)));
|
||||||
state.view_cursor = view_cursor;
|
state.view_cursor = view_cursor;
|
||||||
}
|
}
|
||||||
create_popup
|
create_popup
|
||||||
});
|
});
|
||||||
|
|
||||||
if !create_popup {
|
if !create_popup {
|
||||||
return root_status;
|
return event::Status::Ignored;
|
||||||
}
|
}
|
||||||
#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
|
|
||||||
// TODO emit Message to open menu
|
|
||||||
if self.window_id != window::Id::NONE && self.on_surface_action.is_some() {
|
|
||||||
use crate::surface::action::destroy_popup;
|
|
||||||
use iced_runtime::platform_specific::wayland::popup::{
|
|
||||||
SctkPopupSettings, SctkPositioner,
|
|
||||||
};
|
|
||||||
|
|
||||||
let surface_action = self.on_surface_action.as_ref().unwrap();
|
return root_status.merge(self.create_popup(
|
||||||
let old_active_root = my_state.inner.with_data(|state| {
|
layout,
|
||||||
state
|
view_cursor,
|
||||||
.active_root
|
renderer,
|
||||||
.get(0)
|
shell,
|
||||||
.filter(|r| r.len() == 1)
|
viewport,
|
||||||
.map(|r| r[0])
|
my_state,
|
||||||
});
|
));
|
||||||
|
}
|
||||||
// if position is not on menu bar button skip.
|
Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered)
|
||||||
let position = view_cursor.position();
|
if open && view_cursor.is_over(layout.bounds()) =>
|
||||||
let hovered_root = layout
|
{
|
||||||
.children()
|
return root_status.merge(self.create_popup(
|
||||||
.position(|lo| view_cursor.is_over(lo.bounds()));
|
layout,
|
||||||
|
view_cursor,
|
||||||
let (id, root_list) = my_state.inner.with_data_mut(|state| {
|
renderer,
|
||||||
if let Some(id) = state.popup_id.get(&self.window_id).copied() {
|
shell,
|
||||||
// close existing popups
|
viewport,
|
||||||
state.menu_states.clear();
|
my_state,
|
||||||
state.active_root.clear();
|
));
|
||||||
shell.publish(surface_action(destroy_popup(id)));
|
|
||||||
state.view_cursor = view_cursor;
|
|
||||||
(id, layout.children().map(|lo| lo.bounds()).collect())
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
window::Id::unique(),
|
|
||||||
layout.children().map(|lo| lo.bounds()).collect(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut popup_menu: Menu<'static, _> = Menu {
|
|
||||||
tree: my_state.clone(),
|
|
||||||
menu_roots: std::borrow::Cow::Owned(self.menu_roots.clone()),
|
|
||||||
bounds_expand: self.bounds_expand,
|
|
||||||
menu_overlays_parent: false,
|
|
||||||
close_condition: self.close_condition,
|
|
||||||
item_width: self.item_width,
|
|
||||||
item_height: self.item_height,
|
|
||||||
bar_bounds: layout.bounds(),
|
|
||||||
main_offset: self.main_offset,
|
|
||||||
cross_offset: self.cross_offset,
|
|
||||||
root_bounds_list: root_list,
|
|
||||||
path_highlight: self.path_highlight,
|
|
||||||
style: std::borrow::Cow::Owned(self.style.clone()),
|
|
||||||
position: Point::new(0., 0.),
|
|
||||||
is_overlay: false,
|
|
||||||
window_id: id,
|
|
||||||
depth: 0,
|
|
||||||
on_surface_action: self.on_surface_action.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
init_root_menu(
|
|
||||||
&mut popup_menu,
|
|
||||||
renderer,
|
|
||||||
shell,
|
|
||||||
view_cursor.position().unwrap(),
|
|
||||||
viewport.size(),
|
|
||||||
Vector::new(0., 0.),
|
|
||||||
layout.bounds(),
|
|
||||||
self.main_offset as f32,
|
|
||||||
);
|
|
||||||
let anchor_rect = my_state.inner.with_data_mut(|state| {
|
|
||||||
state.popup_id.insert(self.window_id, id);
|
|
||||||
state.menu_states[0]
|
|
||||||
.iter()
|
|
||||||
.find(|s| s.index.is_none())
|
|
||||||
.map(|s| s.menu_bounds.parent_bounds)
|
|
||||||
.map_or_else(
|
|
||||||
|| {
|
|
||||||
let bounds = layout.bounds();
|
|
||||||
Rectangle {
|
|
||||||
x: bounds.x as i32,
|
|
||||||
y: bounds.y as i32,
|
|
||||||
width: bounds.width as i32,
|
|
||||||
height: bounds.height as i32,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|r| Rectangle {
|
|
||||||
x: r.x as i32,
|
|
||||||
y: r.y as i32,
|
|
||||||
width: r.width as i32,
|
|
||||||
height: r.height as i32,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let menu_node =
|
|
||||||
popup_menu.layout(renderer, Limits::NONE.min_width(1.).min_height(1.));
|
|
||||||
let popup_size = menu_node.size();
|
|
||||||
let positioner = SctkPositioner {
|
|
||||||
size: Some((popup_size.width.ceil() as u32 + 2, popup_size.height.ceil() as u32 + 2)),
|
|
||||||
anchor_rect,
|
|
||||||
anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft,
|
|
||||||
gravity:cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
|
|
||||||
reactive: true,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let parent = self.window_id;
|
|
||||||
shell.publish((surface_action)(crate::surface::action::simple_popup(
|
|
||||||
move || SctkPopupSettings {
|
|
||||||
parent,
|
|
||||||
id,
|
|
||||||
positioner: positioner.clone(),
|
|
||||||
parent_size: None,
|
|
||||||
grab: true,
|
|
||||||
close_with_children: false,
|
|
||||||
input_zone: None,
|
|
||||||
},
|
|
||||||
Some(move || {
|
|
||||||
Element::from(
|
|
||||||
crate::widget::container(popup_menu.clone()).center(Length::Fill),
|
|
||||||
)
|
|
||||||
.map(crate::action::app)
|
|
||||||
}),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Window(Focused) => {
|
// Window(Focused) => {
|
||||||
// my_state.inner.with_data_mut(|state| {
|
// my_state.inner.with_data_mut(|state| {
|
||||||
|
|
|
||||||
|
|
@ -503,7 +503,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> {
|
||||||
if let Some(ms) = data.menu_states.get(self.depth) {
|
if let Some(ms) = data.menu_states.get(self.depth) {
|
||||||
ms.iter()
|
ms.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|ms| self.is_overlay || ms.0 < active_root.len())
|
.filter(|ms| self.is_overlay || ms.0 < 1)
|
||||||
.fold((roots, Vec::new()), |(menu_root, mut nodes), (_i, ms)| {
|
.fold((roots, Vec::new()), |(menu_root, mut nodes), (_i, ms)| {
|
||||||
let slice =
|
let slice =
|
||||||
ms.slice(limits.max(), overlay_offset, self.item_height);
|
ms.slice(limits.max(), overlay_offset, self.item_height);
|
||||||
|
|
@ -564,7 +564,11 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> {
|
||||||
};
|
};
|
||||||
use touch::Event::{FingerLifted, FingerMoved, FingerPressed};
|
use touch::Event::{FingerLifted, FingerMoved, FingerPressed};
|
||||||
|
|
||||||
if !self.tree.inner.with_data(|data| data.open) {
|
if !self
|
||||||
|
.tree
|
||||||
|
.inner
|
||||||
|
.with_data(|data| data.open || data.active_root.len() <= self.depth)
|
||||||
|
{
|
||||||
return (None, Ignored);
|
return (None, Ignored);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -612,11 +616,9 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Mouse(CursorMoved { position }) | Touch(FingerMoved { position, .. }) => {
|
Mouse(CursorMoved { position }) | Touch(FingerMoved { position, .. }) => {
|
||||||
dbg!("moved", self.window_id);
|
|
||||||
let view_cursor = Cursor::Available(position);
|
let view_cursor = Cursor::Available(position);
|
||||||
let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset;
|
let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset;
|
||||||
if !(self.is_overlay || view_cursor.is_over(viewport)) {
|
if !(self.is_overlay || view_cursor.is_over(viewport)) {
|
||||||
dbg!("exit early", view_cursor, viewport);
|
|
||||||
return (None, menu_status);
|
return (None, menu_status);
|
||||||
}
|
}
|
||||||
// dbg!(view_cursor, viewport, self.window_id);
|
// dbg!(view_cursor, viewport, self.window_id);
|
||||||
|
|
@ -665,9 +667,21 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> {
|
||||||
if needs_reset {
|
if needs_reset {
|
||||||
dbg!("reset");
|
dbg!("reset");
|
||||||
if let Some(handler) = self.on_surface_action.as_ref() {
|
if let Some(handler) = self.on_surface_action.as_ref() {
|
||||||
shell.publish((handler)(crate::surface::Action::DestroyPopup(
|
let mut root = self.window_id;
|
||||||
self.window_id,
|
let mut depth = self.depth;
|
||||||
)));
|
while let Some(parent) =
|
||||||
|
state.popup_id.iter().find(|(_, v)| **v == root)
|
||||||
|
{
|
||||||
|
// parent of root popup is the window, so we stop.
|
||||||
|
if depth == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
root = parent.0.clone();
|
||||||
|
depth = depth.saturating_sub(1);
|
||||||
|
}
|
||||||
|
dbg!(root);
|
||||||
|
shell
|
||||||
|
.publish((handler)(crate::surface::Action::DestroyPopup(root)));
|
||||||
}
|
}
|
||||||
|
|
||||||
state.reset();
|
state.reset();
|
||||||
|
|
@ -700,13 +714,12 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> {
|
||||||
view_cursor: Cursor,
|
view_cursor: Cursor,
|
||||||
) {
|
) {
|
||||||
self.tree.inner.with_data(|state| {
|
self.tree.inner.with_data(|state| {
|
||||||
if !state.open {
|
if !state.open || state.active_root.len() <= self.depth {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(active_root) = state.active_root.get(self.depth) else {
|
let Some(active_root) = state.active_root.get(self.depth) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
dbg!(self.depth, &active_root);
|
|
||||||
let viewport = layout.bounds();
|
let viewport = layout.bounds();
|
||||||
let viewport_size = viewport.size();
|
let viewport_size = viewport.size();
|
||||||
let overlay_offset = Point::ORIGIN - viewport.position();
|
let overlay_offset = Point::ORIGIN - viewport.position();
|
||||||
|
|
@ -728,104 +741,103 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> {
|
||||||
),
|
),
|
||||||
|(tree, mt), next_active_root| (tree, &mt[*next_active_root].children),
|
|(tree, mt), next_active_root| (tree, &mt[*next_active_root].children),
|
||||||
);
|
);
|
||||||
|
|
||||||
let indices = state.get_trimmed_indices(self.depth).collect::<Vec<_>>();
|
let indices = state.get_trimmed_indices(self.depth).collect::<Vec<_>>();
|
||||||
|
|
||||||
state.menu_states[self.depth]
|
state.menu_states[self.depth]
|
||||||
.iter()
|
.iter()
|
||||||
.zip(layout.children())
|
.zip(layout.children())
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|ms: &(usize, (&MenuState, Layout<'_>))| {
|
.filter(|ms: &(usize, (&MenuState, Layout<'_>))| self.is_overlay || ms.0 < 1)
|
||||||
self.is_overlay || ms.0 < active_root.len()
|
.fold(
|
||||||
})
|
roots,
|
||||||
.fold(roots, |menu_roots, (i, (ms, children_layout))| {
|
|menu_roots: &Vec<MenuTree<Message>>, (i, (ms, children_layout))| {
|
||||||
let draw_path = self.path_highlight.as_ref().map_or(false, |ph| match ph {
|
let draw_path = self.path_highlight.as_ref().map_or(false, |ph| match ph {
|
||||||
PathHighlight::Full => true,
|
PathHighlight::Full => true,
|
||||||
PathHighlight::OmitActive => !indices.is_empty() && i < indices.len() - 1,
|
PathHighlight::OmitActive => {
|
||||||
PathHighlight::MenuActive => self.depth == state.active_root.len() - 1,
|
!indices.is_empty() && i < indices.len() - 1
|
||||||
});
|
|
||||||
|
|
||||||
// react only to the last menu
|
|
||||||
if self.depth == state.active_root.len() - 1 {
|
|
||||||
view_cursor
|
|
||||||
} else {
|
|
||||||
Cursor::Available([-1.0; 2].into())
|
|
||||||
};
|
|
||||||
dbg!(view_cursor);
|
|
||||||
|
|
||||||
let draw_menu = |r: &mut crate::Renderer| {
|
|
||||||
// calc slice
|
|
||||||
let slice = ms.slice(viewport_size, overlay_offset, self.item_height);
|
|
||||||
let start_index = slice.start_index;
|
|
||||||
let end_index = slice.end_index;
|
|
||||||
|
|
||||||
let children_bounds = children_layout.bounds();
|
|
||||||
|
|
||||||
// draw menu background
|
|
||||||
// let bounds = pad_rectangle(children_bounds, styling.background_expand.into());
|
|
||||||
// println!("cursor: {:?}", view_cursor);
|
|
||||||
// println!("bg_bounds: {:?}", bounds);
|
|
||||||
// println!("color: {:?}\n", styling.background);
|
|
||||||
let menu_quad = renderer::Quad {
|
|
||||||
bounds: pad_rectangle(
|
|
||||||
children_bounds,
|
|
||||||
styling.background_expand.into(),
|
|
||||||
),
|
|
||||||
border: Border {
|
|
||||||
radius: styling.menu_border_radius.into(),
|
|
||||||
width: styling.border_width,
|
|
||||||
color: styling.border_color,
|
|
||||||
},
|
|
||||||
shadow: Shadow::default(),
|
|
||||||
};
|
|
||||||
let menu_color = styling.background;
|
|
||||||
r.fill_quad(menu_quad, menu_color);
|
|
||||||
dbg!(ms.index, children_layout.children().count(), start_index);
|
|
||||||
// draw path hightlight
|
|
||||||
if let (true, Some(active)) = (draw_path, ms.index) {
|
|
||||||
if let Some(active_layout) = children_layout
|
|
||||||
.children()
|
|
||||||
.nth(active.saturating_sub(start_index))
|
|
||||||
{
|
|
||||||
let path_quad = renderer::Quad {
|
|
||||||
bounds: active_layout.bounds(),
|
|
||||||
border: Border {
|
|
||||||
radius: styling.menu_border_radius.into(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
shadow: Shadow::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
r.fill_quad(path_quad, styling.path);
|
|
||||||
}
|
}
|
||||||
}
|
PathHighlight::MenuActive => self.depth == state.active_root.len() - 1,
|
||||||
if start_index < menu_roots.len() {
|
});
|
||||||
// dbg!(start_index, end_index, menu_roots.len());
|
|
||||||
// draw item
|
|
||||||
menu_roots[start_index..=end_index]
|
|
||||||
.iter()
|
|
||||||
.zip(children_layout.children())
|
|
||||||
.for_each(|(mt, clo)| {
|
|
||||||
dbg!(self.depth, view_cursor, clo.bounds());
|
|
||||||
|
|
||||||
mt.item.draw(
|
// react only to the last menu
|
||||||
&active_tree[mt.index],
|
if self.depth == state.active_root.len() - 1 {
|
||||||
r,
|
view_cursor
|
||||||
theme,
|
} else {
|
||||||
style,
|
Cursor::Available([-1.0; 2].into())
|
||||||
clo,
|
};
|
||||||
view_cursor,
|
|
||||||
&children_layout.bounds(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderer.with_layer(render_bounds, draw_menu);
|
let draw_menu = |r: &mut crate::Renderer| {
|
||||||
|
// calc slice
|
||||||
|
let slice = ms.slice(viewport_size, overlay_offset, self.item_height);
|
||||||
|
let start_index = slice.start_index;
|
||||||
|
let end_index = slice.end_index;
|
||||||
|
|
||||||
// only the last menu can have a None active index
|
let children_bounds = children_layout.bounds();
|
||||||
ms.index
|
|
||||||
.map_or(menu_roots, |active| &menu_roots[active].children)
|
// draw menu background
|
||||||
});
|
// let bounds = pad_rectangle(children_bounds, styling.background_expand.into());
|
||||||
|
// println!("cursor: {:?}", view_cursor);
|
||||||
|
// println!("bg_bounds: {:?}", bounds);
|
||||||
|
// println!("color: {:?}\n", styling.background);
|
||||||
|
let menu_quad = renderer::Quad {
|
||||||
|
bounds: pad_rectangle(
|
||||||
|
children_bounds,
|
||||||
|
styling.background_expand.into(),
|
||||||
|
),
|
||||||
|
border: Border {
|
||||||
|
radius: styling.menu_border_radius.into(),
|
||||||
|
width: styling.border_width,
|
||||||
|
color: styling.border_color,
|
||||||
|
},
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
};
|
||||||
|
let menu_color = styling.background;
|
||||||
|
r.fill_quad(menu_quad, menu_color);
|
||||||
|
// draw path hightlight
|
||||||
|
if let (true, Some(active)) = (draw_path, ms.index) {
|
||||||
|
if let Some(active_layout) = children_layout
|
||||||
|
.children()
|
||||||
|
.nth(active.saturating_sub(start_index))
|
||||||
|
{
|
||||||
|
let path_quad = renderer::Quad {
|
||||||
|
bounds: active_layout.bounds(),
|
||||||
|
border: Border {
|
||||||
|
radius: styling.menu_border_radius.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
r.fill_quad(path_quad, styling.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start_index < menu_roots.len() {
|
||||||
|
// dbg!(start_index, end_index, menu_roots.len());
|
||||||
|
// draw item
|
||||||
|
menu_roots[start_index..=end_index]
|
||||||
|
.iter()
|
||||||
|
.zip(children_layout.children())
|
||||||
|
.for_each(|(mt, clo)| {
|
||||||
|
mt.item.draw(
|
||||||
|
&active_tree[mt.index],
|
||||||
|
r,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
clo,
|
||||||
|
view_cursor,
|
||||||
|
&children_layout.bounds(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.with_layer(render_bounds, draw_menu);
|
||||||
|
|
||||||
|
// only the last menu can have a None active index
|
||||||
|
ms.index
|
||||||
|
.map_or(menu_roots, |active| &menu_roots[active].children)
|
||||||
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -914,13 +926,8 @@ impl<'a, Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, cra
|
||||||
shell: &mut Shell<'_, Message>,
|
shell: &mut Shell<'_, Message>,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
) -> event::Status {
|
) -> event::Status {
|
||||||
let prev_hover = self.is_overlay.then(|| {
|
|
||||||
self.tree.inner.with_data(|d| {
|
|
||||||
let menu_states = d.menu_states.get(self.depth).unwrap();
|
|
||||||
menu_states.get(0).and_then(|ms| ms.index)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
let (new_root, status) = self.on_event(event, layout, cursor, renderer, clipboard, shell);
|
let (new_root, status) = self.on_event(event, layout, cursor, renderer, clipboard, shell);
|
||||||
|
|
||||||
// dbg!(new_root.as_ref().map(|r| r.0));
|
// dbg!(new_root.as_ref().map(|r| r.0));
|
||||||
#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
|
#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
|
||||||
if let Some((new_root, new_ms)) = new_root {
|
if let Some((new_root, new_ms)) = new_root {
|
||||||
|
|
@ -928,7 +935,6 @@ impl<'a, Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, cra
|
||||||
SctkPopupSettings, SctkPositioner,
|
SctkPopupSettings, SctkPositioner,
|
||||||
};
|
};
|
||||||
let overlay_offset = Point::ORIGIN - viewport.position();
|
let overlay_offset = Point::ORIGIN - viewport.position();
|
||||||
dbg!(overlay_offset);
|
|
||||||
|
|
||||||
let overlay_cursor = cursor.position().unwrap_or_default() - overlay_offset;
|
let overlay_cursor = cursor.position().unwrap_or_default() - overlay_offset;
|
||||||
|
|
||||||
|
|
@ -941,7 +947,8 @@ impl<'a, Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, cra
|
||||||
.active_root
|
.active_root
|
||||||
.get(self.depth)
|
.get(self.depth)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default(); // dbg!(active_roots);
|
.unwrap_or_default();
|
||||||
|
dbg!(self.window_id, popup_id, active_roots);
|
||||||
// let root_bounds_list = active_roots
|
// let root_bounds_list = active_roots
|
||||||
// .into_iter()
|
// .into_iter()
|
||||||
// .fold(layout, |l, active_root| {
|
// .fold(layout, |l, active_root| {
|
||||||
|
|
@ -989,6 +996,7 @@ impl<'a, Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, cra
|
||||||
depth: self.depth + 1,
|
depth: self.depth + 1,
|
||||||
on_surface_action: self.on_surface_action.clone(),
|
on_surface_action: self.on_surface_action.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// let mut state = self.tree.inner.lock().unwrap();
|
// let mut state = self.tree.inner.lock().unwrap();
|
||||||
// dbg!(state.menu_states.keys());
|
// dbg!(state.menu_states.keys());
|
||||||
// let Some(parent_root) = state.active_root.get(&self.window_id) else {
|
// let Some(parent_root) = state.active_root.get(&self.window_id) else {
|
||||||
|
|
@ -999,12 +1007,15 @@ impl<'a, Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, cra
|
||||||
dbg!("NO ROOT?");
|
dbg!("NO ROOT?");
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut roots = parent_root.clone();
|
let mut roots = parent_root.clone();
|
||||||
|
dbg!(&roots);
|
||||||
|
|
||||||
roots.push(new_root);
|
roots.push(new_root);
|
||||||
// dbg!(&roots);
|
dbg!(&roots);
|
||||||
state.active_root.push(roots);
|
state.active_root.push(roots);
|
||||||
// _ = state.menu_states.remove(&popup_id);
|
dbg!(&state.active_root);
|
||||||
// drop(state);
|
|
||||||
Some((popup_menu, popup_id))
|
Some((popup_menu, popup_id))
|
||||||
}) else {
|
}) else {
|
||||||
return status;
|
return status;
|
||||||
|
|
@ -1022,7 +1033,7 @@ impl<'a, Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, cra
|
||||||
);
|
);
|
||||||
let anchor_rect = self.tree.inner.with_data_mut(|state| {
|
let anchor_rect = self.tree.inner.with_data_mut(|state| {
|
||||||
// let mut state = self.tree.inner.lock().unwrap();
|
// let mut state = self.tree.inner.lock().unwrap();
|
||||||
state.menu_states.get_mut(self.depth).unwrap().push(new_ms);
|
// state.menu_states.get_mut(self.depth).unwrap().push(new_ms);
|
||||||
|
|
||||||
state.menu_states[self.depth]
|
state.menu_states[self.depth]
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -1047,18 +1058,12 @@ impl<'a, Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, cra
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
// dbg!(&anchor_rect);
|
|
||||||
|
|
||||||
// drop(state);
|
|
||||||
let menu_node = Widget::layout(
|
let menu_node = Widget::layout(
|
||||||
&menu,
|
&menu,
|
||||||
&mut Tree::empty(),
|
&mut Tree::empty(),
|
||||||
renderer,
|
renderer,
|
||||||
&Limits::NONE.min_width(1.).min_height(1.),
|
&Limits::NONE.min_width(1.).min_height(1.),
|
||||||
);
|
);
|
||||||
// dbg!(menu_node.size());
|
|
||||||
|
|
||||||
// dbg!(&menu_node);
|
|
||||||
|
|
||||||
let popup_size = menu_node.size();
|
let popup_size = menu_node.size();
|
||||||
let positioner = SctkPositioner {
|
let positioner = SctkPositioner {
|
||||||
|
|
@ -1070,8 +1075,19 @@ impl<'a, Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, cra
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let parent = self.window_id;
|
let parent = self.window_id;
|
||||||
dbg!(&positioner);
|
dbg!(
|
||||||
|
self.depth + 1,
|
||||||
|
&positioner,
|
||||||
|
parent,
|
||||||
|
popup_id,
|
||||||
|
&menu_node,
|
||||||
|
anchor_rect
|
||||||
|
);
|
||||||
|
let ar_len = self
|
||||||
|
.tree
|
||||||
|
.inner
|
||||||
|
.with_data_mut(|state| state.active_root.len());
|
||||||
|
// if self.depth < 1 {
|
||||||
shell.publish((self.on_surface_action.as_ref().unwrap())(
|
shell.publish((self.on_surface_action.as_ref().unwrap())(
|
||||||
crate::surface::action::simple_popup(
|
crate::surface::action::simple_popup(
|
||||||
move || SctkPopupSettings {
|
move || SctkPopupSettings {
|
||||||
|
|
@ -1091,6 +1107,17 @@ impl<'a, Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, cra
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
// } else {
|
||||||
|
// dbg!(SctkPopupSettings {
|
||||||
|
// parent,
|
||||||
|
// id: popup_id,
|
||||||
|
// positioner: positioner.clone(),
|
||||||
|
// parent_size: None,
|
||||||
|
// grab: true,
|
||||||
|
// close_with_children: false,
|
||||||
|
// input_zone: None,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
@ -1133,6 +1160,8 @@ pub(super) fn init_root_menu<Message: Clone>(
|
||||||
.get(menu.depth)
|
.get(menu.depth)
|
||||||
.is_none_or(|s| s.is_empty())
|
.is_none_or(|s| s.is_empty())
|
||||||
&& (!menu.is_overlay || bar_bounds.contains(overlay_cursor)))
|
&& (!menu.is_overlay || bar_bounds.contains(overlay_cursor)))
|
||||||
|
|| menu.depth > 0
|
||||||
|
|| !state.open
|
||||||
{
|
{
|
||||||
// dbg!("exiting from root menu init early...", menu.depth);
|
// dbg!("exiting from root menu init early...", menu.depth);
|
||||||
return;
|
return;
|
||||||
|
|
@ -1436,13 +1465,26 @@ fn process_menu_events<'b, Message: std::clone::Clone>(
|
||||||
|
|
||||||
// get active item
|
// get active item
|
||||||
// let mt = indices.iter().fold(root | mt, &i | &mut mt.children[i]);
|
// let mt = indices.iter().fold(root | mt, &i | &mut mt.children[i]);
|
||||||
let (tree, mt) = indices.iter().take(indices.len()).fold(
|
// let (tree, mt) = indices.iter().take(indices.len()).fold(
|
||||||
(
|
// (
|
||||||
&mut state.tree.children[active_root[0]].children,
|
// &mut state.tree.children[active_root[0]].children,
|
||||||
&mut menu_roots[active_root[0]],
|
// &mut menu_roots[active_root[0]],
|
||||||
),
|
// ),
|
||||||
|(tree, mt), next_active_root| (tree, &mut mt.children[*next_active_root]),
|
// |(tree, mt), next_active_root| {
|
||||||
);
|
// dbg!(mt.children.len(), next_active_root);
|
||||||
|
// (tree, &mut mt.children[*next_active_root])
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
let (tree, mt) = active_root
|
||||||
|
.iter()
|
||||||
|
.skip(if menu.is_overlay { 0 } else { 1 })
|
||||||
|
.fold(
|
||||||
|
(
|
||||||
|
&mut state.tree.children[active_root[0]].children,
|
||||||
|
&mut menu_roots[active_root[0]],
|
||||||
|
),
|
||||||
|
|(tree, mt), next_active_root| (tree, &mut mt.children[*next_active_root]),
|
||||||
|
);
|
||||||
// let Some(i) = state.menu_states[menu.depth].iter().position(|ms| {
|
// let Some(i) = state.menu_states[menu.depth].iter().position(|ms| {
|
||||||
// ms.menu_bounds
|
// ms.menu_bounds
|
||||||
// .check_bounds
|
// .check_bounds
|
||||||
|
|
@ -1509,7 +1551,6 @@ where
|
||||||
|
|
||||||
menu.tree.inner.with_data_mut(|state| {
|
menu.tree.inner.with_data_mut(|state| {
|
||||||
let Some(active_root) = state.active_root.get(menu.depth).clone() else {
|
let Some(active_root) = state.active_root.get(menu.depth).clone() else {
|
||||||
panic!();
|
|
||||||
if is_overlay && !menu.bar_bounds.contains(overlay_cursor) {
|
if is_overlay && !menu.bar_bounds.contains(overlay_cursor) {
|
||||||
state.reset();
|
state.reset();
|
||||||
}
|
}
|
||||||
|
|
@ -1525,11 +1566,18 @@ where
|
||||||
state.view_cursor = view_cursor;
|
state.view_cursor = view_cursor;
|
||||||
|
|
||||||
// * remove invalid menus
|
// * remove invalid menus
|
||||||
|
dbg!(
|
||||||
|
&state
|
||||||
|
.menu_states
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.iter().map(|s| s.index).collect::<Vec<_>>())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
let mut prev_bounds = std::iter::once(menu.bar_bounds)
|
let mut prev_bounds = std::iter::once(menu.bar_bounds)
|
||||||
.chain(
|
.chain(
|
||||||
state.menu_states[menu.depth][..state.menu_states.len().saturating_sub(1).min(1)]
|
(state.menu_states[..menu.depth])
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ms| ms.menu_bounds.children_bounds),
|
.map(|s| s[0].menu_bounds.children_bounds),
|
||||||
)
|
)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let menu_states = state.menu_states.get_mut(menu.depth).unwrap();
|
let menu_states = state.menu_states.get_mut(menu.depth).unwrap();
|
||||||
|
|
@ -1569,7 +1617,6 @@ where
|
||||||
let should_add = is_overlay || menu_states.len() < 2;
|
let should_add = is_overlay || menu_states.len() < 2;
|
||||||
dbg!(&indices);
|
dbg!(&indices);
|
||||||
|
|
||||||
let menu_state_len = menu_states.len();
|
|
||||||
// dbg!(menu_states.iter().map(|m| m.index).collect::<Vec<_>>());
|
// dbg!(menu_states.iter().map(|m| m.index).collect::<Vec<_>>());
|
||||||
// * update active item
|
// * update active item
|
||||||
let Some(last_menu_state) = menu_states.get_mut(0) else {
|
let Some(last_menu_state) = menu_states.get_mut(0) else {
|
||||||
|
|
@ -1621,6 +1668,9 @@ where
|
||||||
),
|
),
|
||||||
|(tree, mt), next_active_root| (tree, &mt[*next_active_root].children),
|
|(tree, mt), next_active_root| (tree, &mt[*next_active_root].children),
|
||||||
);
|
);
|
||||||
|
if is_overlay {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
let active_menu = if is_overlay {
|
let active_menu = if is_overlay {
|
||||||
indices[0..indices.len().saturating_sub(1)].iter().fold(
|
indices[0..indices.len().saturating_sub(1)].iter().fold(
|
||||||
roots,
|
roots,
|
||||||
|
|
@ -1648,27 +1698,19 @@ where
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// if last_menu_state
|
let remove = last_menu_state
|
||||||
// .index
|
|
||||||
// .as_ref()
|
|
||||||
// .is_some_and(|old_index| *old_index == new_index)
|
|
||||||
// {
|
|
||||||
// dbg!("skipping duplicate", last_menu_state.index);
|
|
||||||
// return (None, Captured);
|
|
||||||
// }
|
|
||||||
if last_menu_state
|
|
||||||
.index
|
.index
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|i| *i != new_index)
|
.is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty());
|
||||||
{
|
|
||||||
shell.publish((menu.on_surface_action.as_ref().unwrap())(
|
#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
|
||||||
crate::surface::action::destroy_popup(
|
if remove {
|
||||||
*state
|
if let Some(id) = state.popup_id.remove(&menu.window_id) {
|
||||||
.popup_id
|
state.active_root.pop();
|
||||||
.entry(menu.window_id)
|
shell.publish((menu.on_surface_action.as_ref().unwrap())({
|
||||||
.or_insert_with(window::Id::unique),
|
crate::surface::action::destroy_popup(id)
|
||||||
),
|
}))
|
||||||
));
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let item = &active_menu[new_index];
|
let item = &active_menu[new_index];
|
||||||
|
|
@ -1737,6 +1779,9 @@ where
|
||||||
v.push(ms);
|
v.push(ms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if remove {
|
||||||
|
state.menu_states.pop();
|
||||||
|
}
|
||||||
|
|
||||||
(new_menu_root, Captured)
|
(new_menu_root, Captured)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue