Rework shortcut selector ui in settings

This commit is contained in:
Exidex 2025-03-30 19:25:02 +02:00
parent aacf78a377
commit ae0f833f15
No known key found for this signature in database
GPG key ID: AC63AA86DD4F2D45
6 changed files with 416 additions and 290 deletions

View file

@ -76,7 +76,7 @@ impl PluginWidgetContainer {
self.images = images;
// use new state with values from old state but only widget ids which exists in new state
// so we this way we use already existing values but remove state for removed widgets
// so this way we use already existing values but remove state for removed widgets
let old_state = mem::replace(&mut self.state, create_state(&container));
for (key, value) in old_state.into_iter() {

View file

@ -1,10 +1,13 @@
use gauntlet_common::model::EntrypointId;
use gauntlet_common::model::PhysicalShortcut;
use gauntlet_common::model::PluginId;
use gauntlet_common_ui::physical_key_model;
use gauntlet_common_ui::shortcut_to_text;
use iced::advanced::graphics::core::event;
use iced::advanced::graphics::core::keyboard;
use iced::advanced::layout;
use iced::advanced::mouse;
use iced::advanced::overlay;
use iced::advanced::renderer;
use iced::advanced::widget::tree;
use iced::advanced::widget::Tree;
@ -12,95 +15,123 @@ use iced::advanced::Clipboard;
use iced::advanced::Layout;
use iced::advanced::Shell;
use iced::advanced::Widget;
use iced::alignment;
use iced::keyboard::key::Physical;
use iced::mouse::Button;
use iced::widget::column;
use iced::widget::container;
use iced::widget::container::draw_background;
use iced::widget::container::layout;
use iced::widget::row;
use iced::widget::text;
use iced::Element;
use iced::Alignment;
use iced::Event;
use iced::Length;
use iced::Padding;
use iced::Point;
use iced::Rectangle;
use iced::Renderer;
use iced::Size;
use iced::Vector;
pub struct ShortcutSelector<'a, Message, Theme>
where
Theme: Catalog + text::Catalog + container::Catalog,
{
padding: Padding,
width: Length,
height: Length,
max_width: f32,
max_height: f32,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
use crate::theme::text::TextStyle;
use crate::theme::Element;
use crate::theme::GauntletSettingsTheme;
on_shortcut_captured: Box<dyn Fn(Option<PhysicalShortcut>) -> Message + 'a>,
on_capturing_change: Box<dyn Fn(bool) -> Message + 'a>,
content: Element<'a, Message, Theme>,
pub struct ShortcutData {
pub shortcut: Option<PhysicalShortcut>,
pub error: Option<String>,
}
impl<'a, Message: 'a, Theme> ShortcutSelector<'a, Message, Theme>
pub fn shortcut_selector<'a, 'b: 'a, 'c, Message: 'a, Id: 'a, F>(
shortcut_id: Id,
current_shortcut: &'b ShortcutData,
on_shortcut_captured: F,
overlay_class: <GauntletSettingsTheme as container::Catalog>::Class<'a>,
) -> Element<'a, Message>
where
Theme: Catalog + text::Catalog + container::Catalog + 'a,
F: 'a + Fn(Id, Option<PhysicalShortcut>) -> Message,
Id: Clone,
{
pub fn new<F, F2>(
current_shortcut: &Option<PhysicalShortcut>,
Element::new(ShortcutSelector::new::<F>(
shortcut_id,
current_shortcut,
on_shortcut_captured,
overlay_class,
))
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum ShortcutId {
Global,
Entrypoint {
plugin_id: PluginId,
entrypoint_id: EntrypointId,
},
}
pub struct ShortcutSelector<'a, 'b, Message, Id>
where
Id: Clone,
{
on_shortcut_captured: Box<dyn Fn(Id, Option<PhysicalShortcut>) -> Message + 'a>,
shortcut_id: Id,
current_shortcut: &'b ShortcutData,
content: Element<'a, Message>,
popup: Element<'a, Message>,
overlay_class: <GauntletSettingsTheme as container::Catalog>::Class<'a>,
}
impl<'a, 'b, 'c, Message: 'a, Id> ShortcutSelector<'a, 'b, Message, Id>
where
Id: Clone,
{
pub fn new<F>(
shortcut_id: Id,
current_shortcut: &'b ShortcutData,
on_shortcut_captured: F,
on_capturing_change: F2,
overlay_class: <GauntletSettingsTheme as container::Catalog>::Class<'a>,
) -> Self
where
F: 'a + Fn(Option<PhysicalShortcut>) -> Message,
F2: 'a + Fn(bool) -> Message,
F: 'a + Fn(Id, Option<PhysicalShortcut>) -> Message,
{
let mut content: Vec<Element<Message, Theme>> = vec![];
let content = render_shortcut(&current_shortcut.shortcut);
if let Some(current_shortcut) = current_shortcut {
let (key_name, alt_modifier_text, meta_modifier_text, control_modifier_text, shift_modifier_text) =
shortcut_to_text(current_shortcut);
let content = container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x(Length::Fill)
.center_y(Length::Fill)
.into();
if let Some(meta_modifier_text) = meta_modifier_text {
content.push(meta_modifier_text);
}
let recording_text: Element<_> = text("Recording shortcut...").into();
if let Some(control_modifier_text) = control_modifier_text {
content.push(control_modifier_text);
}
let backspace_test: Element<_> = text("Backspace - Unset Shortcut").class(TextStyle::Subtitle).into();
if let Some(shift_modifier_text) = shift_modifier_text {
content.push(shift_modifier_text);
}
let escape_test: Element<_> = text("Escape - Stop Capturing").class(TextStyle::Subtitle).into();
if let Some(alt_modifier_text) = alt_modifier_text {
content.push(alt_modifier_text);
}
let popup: Element<_> = column(vec![recording_text, backspace_test, escape_test])
.align_x(Alignment::Center)
.into();
content.push(key_name);
}
let content: Element<_, _> = row(content).spacing(8.0).into();
let content = container(content).into();
let popup = container(popup)
.max_height(80)
.center_x(Length::Fill)
.center_y(Length::Fill)
.max_width(300)
.width(Length::Fill)
.height(Length::Fill)
.into();
Self {
padding: Padding::ZERO,
width: Length::Fill,
height: Length::Fill,
max_width: f32::INFINITY,
max_height: f32::INFINITY,
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
on_shortcut_captured: Box::new(on_shortcut_captured),
on_capturing_change: Box::new(on_capturing_change),
shortcut_id,
current_shortcut,
content,
popup,
overlay_class,
}
}
}
@ -110,36 +141,26 @@ struct State {
is_capturing: bool,
}
impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for ShortcutSelector<'a, Message, Theme>
impl<'a, 'b, Message: 'a, Id> Widget<Message, GauntletSettingsTheme, Renderer> for ShortcutSelector<'a, 'b, Message, Id>
where
Theme: Catalog + text::Catalog + container::Catalog,
Id: Clone,
{
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
width: Length::Fill,
height: Length::Fill,
}
}
fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
layout(
limits,
self.width,
self.height,
self.max_width,
self.max_height,
self.padding,
self.horizontal_alignment,
self.vertical_alignment,
|limits| self.content.as_widget().layout(tree, renderer, limits),
)
self.content.as_widget().layout(&mut tree.children[0], renderer, limits)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
theme: &GauntletSettingsTheme,
renderer_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
@ -153,18 +174,16 @@ where
Status::Active
};
let style = Catalog::style(theme, &<Theme as Catalog>::default(), style);
let style = Catalog::style(theme, &<GauntletSettingsTheme as Catalog>::default(), style);
draw_background(renderer, &style, layout.bounds());
self.content.as_widget().draw(
tree,
&tree.children[0],
renderer,
theme,
&renderer::Style {
text_color: renderer_style.text_color,
},
layout.children().next().unwrap(),
renderer_style,
layout,
cursor,
viewport,
);
@ -179,11 +198,11 @@ where
}
fn children(&self) -> Vec<Tree> {
self.content.as_widget().children()
vec![Tree::new(&self.content), Tree::new(&self.popup)]
}
fn diff(&self, tree: &mut Tree) {
self.content.as_widget().diff(tree);
tree.diff_children(&[self.content.as_widget(), self.popup.as_widget()]);
}
fn on_event(
@ -211,21 +230,18 @@ where
match physical_key {
Physical::Code(code) => {
match code {
keyboard::key::Code::Backspace => {
keyboard::key::Code::Backspace if modifiers.is_empty() => {
state.is_capturing = false;
let message = (self.on_capturing_change)(false);
shell.publish(message);
let message = (self.on_shortcut_captured)(None);
let message = (self.on_shortcut_captured)(self.shortcut_id.clone(), None);
shell.publish(message);
event::Status::Ignored
}
keyboard::key::Code::Escape => {
keyboard::key::Code::Escape if modifiers.is_empty() => {
state.is_capturing = false;
let message = (self.on_capturing_change)(false);
let message = (self.on_shortcut_captured)(self.shortcut_id.clone(), None);
shell.publish(message);
event::Status::Ignored
@ -236,10 +252,10 @@ where
Some(shortcut) => {
state.is_capturing = false;
let message = (self.on_capturing_change)(false);
shell.publish(message);
let message = (self.on_shortcut_captured)(Some(shortcut));
let message = (self.on_shortcut_captured)(
self.shortcut_id.clone(),
Some(shortcut),
);
shell.publish(message);
event::Status::Captured
@ -263,16 +279,10 @@ where
if cursor.is_over(layout.bounds()) {
state.is_capturing = true;
let message = (self.on_capturing_change)(true);
shell.publish(message);
event::Status::Captured
} else {
state.is_capturing = false;
let message = (self.on_capturing_change)(false);
shell.publish(message);
event::Status::Ignored
}
}
@ -297,15 +307,40 @@ where
mouse::Interaction::default()
}
}
}
impl<'a, Message, Theme> From<ShortcutSelector<'a, Message, Theme>> for Element<'a, Message, Theme>
where
Message: 'a,
Theme: Catalog + text::Catalog + container::Catalog + 'a,
{
fn from(shortcut_selector: ShortcutSelector<'a, Message, Theme>) -> Self {
Self::new(shortcut_selector)
fn overlay<'c>(
&'c mut self,
tree: &'c mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'c, Message, GauntletSettingsTheme, Renderer>> {
let state = tree.state.downcast_ref::<State>();
let mut children = tree.children.iter_mut();
let content = self
.content
.as_widget_mut()
.overlay(children.next().unwrap(), layout, renderer, translation);
let popup = if state.is_capturing {
Some(overlay::Element::new(Box::new(Overlay {
position: layout.position() + translation,
popup: &self.popup,
state: children.next().unwrap(),
content_bounds: layout.bounds(),
class: &self.overlay_class,
})))
} else {
None
};
if content.is_some() || popup.is_some() {
Some(overlay::Group::with_children(content.into_iter().chain(popup).collect()).overlay())
} else {
None
}
}
}
@ -322,3 +357,121 @@ pub trait Catalog {
fn style(&self, class: &Self::Class<'_>, status: Status) -> container::Style;
}
pub fn render_shortcut<'a, Message: 'a>(shortcut: &Option<PhysicalShortcut>) -> Element<'a, Message> {
let mut content: Vec<Element<Message>> = vec![];
if let Some(current_shortcut) = shortcut {
let (key_name, alt_modifier_text, meta_modifier_text, control_modifier_text, shift_modifier_text) =
shortcut_to_text(current_shortcut);
if let Some(meta_modifier_text) = meta_modifier_text {
content.push(meta_modifier_text);
}
if let Some(control_modifier_text) = control_modifier_text {
content.push(control_modifier_text);
}
if let Some(shift_modifier_text) = shift_modifier_text {
content.push(shift_modifier_text);
}
if let Some(alt_modifier_text) = alt_modifier_text {
content.push(alt_modifier_text);
}
content.push(key_name);
}
let content: Element<Message> = row(content).spacing(8.0).into();
content
}
struct Overlay<'a, 'b, Message> {
position: Point,
popup: &'b Element<'a, Message>,
state: &'b mut Tree,
content_bounds: Rectangle,
class: &'b <GauntletSettingsTheme as container::Catalog>::Class<'a>,
}
impl<'a, 'b, Message> overlay::Overlay<Message, GauntletSettingsTheme, Renderer> for Overlay<'a, 'b, Message> {
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
let padding = 2.0;
let gap = 10.0;
let viewport = Rectangle::with_size(bounds);
let popup_layout = self.popup.as_widget().layout(
self.state,
renderer,
&layout::Limits::new(Size::ZERO, viewport.size()).shrink(Padding::new(padding)),
);
let text_bounds = popup_layout.bounds();
let x_center = self.position.x + (self.content_bounds.width - text_bounds.width) / 2.0;
let mut tooltip_bounds = {
let offset = Vector::new(x_center, self.position.y + self.content_bounds.height + gap + padding);
Rectangle {
x: offset.x - padding,
y: offset.y - padding,
width: text_bounds.width + padding * 2.0,
height: text_bounds.height + padding * 2.0,
}
};
// snap_within_viewport
if tooltip_bounds.x < viewport.x {
tooltip_bounds.x = viewport.x;
} else if viewport.x + viewport.width < tooltip_bounds.x + tooltip_bounds.width {
tooltip_bounds.x = viewport.x + viewport.width - tooltip_bounds.width;
}
if tooltip_bounds.y < viewport.y {
tooltip_bounds.y = viewport.y;
} else if viewport.y + viewport.height < tooltip_bounds.y + tooltip_bounds.height {
tooltip_bounds.y = viewport.y + viewport.height - tooltip_bounds.height;
}
layout::Node::with_children(
tooltip_bounds.size(),
vec![popup_layout.translate(Vector::new(padding, padding))],
)
.translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &GauntletSettingsTheme,
inherited_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
) {
let style = <GauntletSettingsTheme as container::Catalog>::style(theme, self.class);
draw_background(renderer, &style, layout.bounds());
let defaults = renderer::Style {
text_color: style.text_color.unwrap_or(inherited_style.text_color),
};
self.popup.as_widget().draw(
self.state,
renderer,
theme,
&defaults,
layout.children().next().unwrap(),
cursor_position,
&Rectangle::with_size(Size::INFINITY),
);
}
fn is_over(&self, _layout: Layout<'_>, _renderer: &Renderer, _cursor_position: Point) -> bool {
false
}
}

View file

@ -79,7 +79,7 @@ struct ManagementAppModel {
}
#[derive(Debug, Clone)]
enum ManagementAppMsg {
pub enum ManagementAppMsg {
FontLoaded(Result<(), font::Error>),
General(ManagementAppGeneralMsgIn),
Plugin(ManagementAppPluginMsgIn),
@ -191,34 +191,16 @@ fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task<Man
ManagementAppMsg::Plugin(message) => {
state.plugins_state.update(message).map(|msg| {
match msg {
ManagementAppPluginMsgOut::PluginsReloaded(plugins) => {
ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::PluginsFetched(plugins))
}
ManagementAppPluginMsgOut::Noop => ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::Noop),
ManagementAppPluginMsgOut::DownloadPlugin { plugin_id } => {
ManagementAppMsg::DownloadPlugin { plugin_id }
}
ManagementAppPluginMsgOut::SelectedItem(selected_item) => {
ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::SelectItem(selected_item))
}
ManagementAppPluginMsgOut::HandleBackendError(err) => ManagementAppMsg::HandleBackendError(err),
ManagementAppPluginMsgOut::Inner(msg) => ManagementAppMsg::Plugin(msg),
ManagementAppPluginMsgOut::Outer(msg) => msg,
}
})
}
ManagementAppMsg::General(message) => {
state.general_state.update(message).map(|msg| {
match msg {
ManagementAppGeneralMsgOut::Noop => ManagementAppMsg::General(ManagementAppGeneralMsgIn::Noop),
ManagementAppGeneralMsgOut::HandleBackendError(err) => ManagementAppMsg::HandleBackendError(err),
ManagementAppGeneralMsgOut::SetGlobalShortcutResponse {
shortcut,
shortcut_error,
} => {
ManagementAppMsg::General(ManagementAppGeneralMsgIn::SetGlobalShortcutResponse {
shortcut,
shortcut_error,
})
}
ManagementAppGeneralMsgOut::Inner(msg) => ManagementAppMsg::General(msg),
ManagementAppGeneralMsgOut::Outer(msg) => msg,
}
})
}
@ -266,7 +248,7 @@ fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task<Man
},
|result| {
handle_backend_error(result, |plugins| {
ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::PluginsFetched(plugins))
ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::PluginsReloaded(plugins))
})
},
)

View file

@ -22,27 +22,28 @@ use iced::Task;
use iced_fonts::Bootstrap;
use iced_fonts::BOOTSTRAP_FONT;
use crate::components::shortcut_selector::ShortcutSelector;
use crate::components::shortcut_selector::shortcut_selector;
use crate::components::shortcut_selector::ShortcutData;
use crate::components::shortcut_selector::ShortcutId;
use crate::theme::container::ContainerStyle;
use crate::theme::text::TextStyle;
use crate::theme::Element;
use crate::ui::ManagementAppMsg;
pub struct ManagementAppGeneralState {
backend_api: Option<BackendApi>,
theme: SettingsTheme,
window_position_mode: WindowPositionMode,
current_shortcut: Option<PhysicalShortcut>,
current_shortcut_error: Option<String>,
currently_capturing: bool,
current_shortcut: ShortcutData,
}
#[derive(Debug, Clone)]
pub enum ManagementAppGeneralMsgIn {
ShortcutCaptured(Option<PhysicalShortcut>),
CapturingChanged(bool),
ShortcutCaptured(ShortcutId, Option<PhysicalShortcut>),
ThemeChanged(SettingsTheme),
WindowPositionModeChanged(WindowPositionMode),
SetGlobalShortcutResponse {
HandleShortcutResponse {
id: ShortcutId,
shortcut: Option<PhysicalShortcut>,
shortcut_error: Option<String>,
},
@ -57,12 +58,8 @@ pub enum ManagementAppGeneralMsgIn {
#[derive(Debug, Clone)]
pub enum ManagementAppGeneralMsgOut {
Noop,
SetGlobalShortcutResponse {
shortcut: Option<PhysicalShortcut>,
shortcut_error: Option<String>,
},
HandleBackendError(BackendApiError),
Inner(ManagementAppGeneralMsgIn),
Outer(ManagementAppMsg),
}
impl ManagementAppGeneralState {
@ -71,9 +68,10 @@ impl ManagementAppGeneralState {
backend_api,
theme: SettingsTheme::AutoDetect,
window_position_mode: WindowPositionMode::Static,
current_shortcut: None,
current_shortcut_error: None,
currently_capturing: false,
current_shortcut: ShortcutData {
shortcut: None,
error: None,
},
}
}
@ -84,7 +82,7 @@ impl ManagementAppGeneralState {
};
match message {
ManagementAppGeneralMsgIn::ShortcutCaptured(shortcut) => {
ManagementAppGeneralMsgIn::ShortcutCaptured(id, shortcut) => {
let mut backend_api = backend_api.clone();
Task::perform(
@ -99,12 +97,14 @@ impl ManagementAppGeneralState {
},
move |result| {
let shortcut = shortcut.clone();
let id = id.clone();
handle_backend_error(result, move |shortcut_error| {
ManagementAppGeneralMsgOut::SetGlobalShortcutResponse {
ManagementAppGeneralMsgOut::Inner(ManagementAppGeneralMsgIn::HandleShortcutResponse {
id,
shortcut,
shortcut_error,
}
})
})
},
)
@ -118,13 +118,10 @@ impl ManagementAppGeneralState {
} => {
self.theme = theme;
self.window_position_mode = window_position_mode;
self.current_shortcut = shortcut;
self.current_shortcut_error = shortcut_error;
Task::done(ManagementAppGeneralMsgOut::Noop)
}
ManagementAppGeneralMsgIn::CapturingChanged(capturing) => {
self.currently_capturing = capturing;
self.current_shortcut = ShortcutData {
shortcut,
error: shortcut_error,
};
Task::none()
}
@ -139,7 +136,9 @@ impl ManagementAppGeneralState {
Ok(())
},
|result| handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Noop),
|result| {
handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Outer(ManagementAppMsg::Noop))
},
)
}
ManagementAppGeneralMsgIn::WindowPositionModeChanged(mode) => {
@ -153,15 +152,20 @@ impl ManagementAppGeneralState {
Ok(())
},
|result| handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Noop),
|result| {
handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Outer(ManagementAppMsg::Noop))
},
)
}
ManagementAppGeneralMsgIn::SetGlobalShortcutResponse {
ManagementAppGeneralMsgIn::HandleShortcutResponse {
id,
shortcut,
shortcut_error,
} => {
self.current_shortcut = shortcut;
self.current_shortcut_error = shortcut_error;
self.current_shortcut = ShortcutData {
shortcut,
error: shortcut_error,
};
Task::none()
}
@ -169,12 +173,12 @@ impl ManagementAppGeneralState {
}
pub fn view(&self) -> Element<ManagementAppGeneralMsgIn> {
let global_shortcut_selector: Element<_> = ShortcutSelector::new(
let global_shortcut_selector = shortcut_selector(
ShortcutId::Global,
&self.current_shortcut,
move |value| ManagementAppGeneralMsgIn::ShortcutCaptured(value),
move |value| ManagementAppGeneralMsgIn::CapturingChanged(value),
)
.into();
move |id, shortcut| ManagementAppGeneralMsgIn::ShortcutCaptured(id, shortcut),
ContainerStyle::Box,
);
let global_shortcut_field: Element<_> = container(global_shortcut_selector)
.width(Length::Fill)
@ -200,8 +204,6 @@ impl ManagementAppGeneralState {
let content: Element<_> = container(content).width(Length::Fill).into();
let content: Element<_> = container(content).width(Length::Fill).into();
content
}
@ -290,59 +292,41 @@ impl ManagementAppGeneralState {
}
fn shortcut_capture_after(&self) -> Element<ManagementAppGeneralMsgIn> {
if self.currently_capturing {
let hint1: Element<_> = text("Backspace - Unset Shortcut")
.width(Length::Fill)
.class(TextStyle::Subtitle)
if let Some(current_shortcut_error) = &self.current_shortcut.error {
let error_icon: Element<_> = value(Bootstrap::ExclamationTriangleFill)
.font(BOOTSTRAP_FONT)
.class(TextStyle::Destructive)
.into();
let hint2: Element<_> = text("Escape - Stop Capturing")
.width(Length::Fill)
.class(TextStyle::Subtitle)
let error_text: Element<_> = text(current_shortcut_error).class(TextStyle::Destructive).into();
let error_text: Element<_> = container(error_text)
.padding(16.0)
.max_width(300)
.class(ContainerStyle::Box)
.into();
column(vec![hint1, hint2])
let tooltip: Element<_> = tooltip(error_icon, error_text, Position::Bottom).into();
let content = container(tooltip)
.width(Length::FillPortion(3))
.align_x(Alignment::Center)
.align_y(alignment::Vertical::Center)
.padding(Padding::from([0.0, 8.0]))
.into()
.into();
content
} else {
if let Some(current_shortcut_error) = &self.current_shortcut_error {
let error_icon: Element<_> = value(Bootstrap::ExclamationTriangleFill)
.font(BOOTSTRAP_FONT)
.class(TextStyle::Destructive)
.into();
let error_text: Element<_> = text(current_shortcut_error).class(TextStyle::Destructive).into();
let error_text: Element<_> = container(error_text)
.padding(16.0)
.max_width(300)
.class(ContainerStyle::Box)
.into();
let tooltip: Element<_> = tooltip(error_icon, error_text, Position::Bottom).into();
let content = container(tooltip)
.width(Length::FillPortion(3))
.align_y(alignment::Vertical::Center)
.padding(Padding::from([0.0, 8.0]))
.into();
content
} else {
Space::with_width(Length::FillPortion(3)).into()
}
Space::with_width(Length::FillPortion(3)).into()
}
}
}
pub fn handle_backend_error<T>(
fn handle_backend_error<T>(
result: Result<T, BackendApiError>,
convert: impl FnOnce(T) -> ManagementAppGeneralMsgOut,
) -> ManagementAppGeneralMsgOut {
match result {
Ok(val) => convert(val),
Err(err) => ManagementAppGeneralMsgOut::HandleBackendError(err),
Err(err) => ManagementAppGeneralMsgOut::Outer(ManagementAppMsg::HandleBackendError(err)),
}
}

View file

@ -32,13 +32,13 @@ use iced_fonts::BOOTSTRAP_FONT;
use crate::theme::button::ButtonStyle;
use crate::theme::text::TextStyle;
use crate::theme::Element;
use crate::ui::ManagementAppMsg;
use crate::views::plugins::preferences::preferences_ui;
use crate::views::plugins::preferences::PluginPreferencesMsg;
use crate::views::plugins::preferences::SelectItem;
use crate::views::plugins::table::PluginTableMsgIn;
use crate::views::plugins::table::PluginTableMsgOut;
use crate::views::plugins::table::PluginTableState;
use crate::views::plugins::table::PluginTableUpdateResult;
mod preferences;
mod table;
@ -48,19 +48,17 @@ pub enum ManagementAppPluginMsgIn {
PluginTableMsg(PluginTableMsgIn),
PluginPreferenceMsg(PluginPreferencesMsg),
FetchPlugins,
PluginsFetched(HashMap<PluginId, SettingsPlugin>),
PluginsReloaded(HashMap<PluginId, SettingsPlugin>),
RemovePlugin { plugin_id: PluginId },
ToggleShowEntrypoint { plugin_id: PluginId },
DownloadPlugin { plugin_id: PluginId },
SelectItem(SelectedItem),
Noop,
}
pub enum ManagementAppPluginMsgOut {
PluginsReloaded(HashMap<PluginId, SettingsPlugin>),
SelectedItem(SelectedItem),
DownloadPlugin { plugin_id: PluginId },
HandleBackendError(BackendApiError),
Noop,
Inner(ManagementAppPluginMsgIn),
Outer(ManagementAppMsg),
}
pub struct ManagementAppPluginsState {
@ -99,7 +97,7 @@ impl ManagementAppPluginsState {
tracing::debug!("Opening selected item: {:?}", select_item);
Self {
backend_api,
backend_api: backend_api.clone(),
plugin_data: Rc::new(RefCell::new(PluginDataContainer::new())),
preference_user_data: HashMap::new(),
selected_item: select_item,
@ -115,71 +113,77 @@ impl ManagementAppPluginsState {
match message {
ManagementAppPluginMsgIn::PluginTableMsg(message) => {
match self.table_state.update(message) {
PluginTableUpdateResult::Command(command) => command.map(|_| ManagementAppPluginMsgOut::Noop),
PluginTableUpdateResult::Value(msg) => {
match msg {
PluginTableMsgOut::SetPluginState { enabled, plugin_id } => {
let mut backend_client = backend_api.clone();
self.table_state.update(message).then(move |msg| {
match msg {
PluginTableMsgOut::SetPluginState { enabled, plugin_id } => {
let mut backend_client = backend_api.clone();
Task::perform(
async move {
backend_client.set_plugin_state(plugin_id, enabled).await?;
Task::perform(
async move {
backend_client.set_plugin_state(plugin_id, enabled).await?;
let plugins = backend_client.plugins().await?;
let plugins = backend_client.plugins().await?;
Ok(plugins)
},
|result| {
handle_backend_error(result, |plugins| {
ManagementAppPluginMsgOut::PluginsReloaded(plugins)
})
},
)
}
PluginTableMsgOut::SetEntrypointState {
enabled,
plugin_id,
entrypoint_id,
} => {
let mut backend_client = backend_api.clone();
Ok(plugins)
},
|result| {
handle_backend_error(result, |plugins| {
ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded(
plugins,
))
})
},
)
}
PluginTableMsgOut::SetEntrypointState {
enabled,
plugin_id,
entrypoint_id,
} => {
let mut backend_client = backend_api.clone();
Task::perform(
async move {
backend_client
.set_entrypoint_state(plugin_id, entrypoint_id, enabled)
.await?;
Task::perform(
async move {
backend_client
.set_entrypoint_state(plugin_id, entrypoint_id, enabled)
.await?;
let plugins = backend_client.plugins().await?;
let plugins = backend_client.plugins().await?;
Ok(plugins)
},
|result| {
handle_backend_error(result, |plugins| {
ManagementAppPluginMsgOut::PluginsReloaded(plugins)
})
},
)
}
PluginTableMsgOut::SelectItem(selected_item) => {
Task::done(ManagementAppPluginMsgOut::SelectedItem(selected_item))
}
PluginTableMsgOut::ToggleShowEntrypoints { plugin_id } => {
let plugins = {
let mut plugin_data = self.plugin_data.borrow_mut();
let settings_plugin_data = plugin_data.plugins_state.get_mut(&plugin_id).unwrap();
settings_plugin_data.show_entrypoints = !settings_plugin_data.show_entrypoints;
plugin_data.plugins.clone()
};
self.apply_plugin_fetch(plugins);
Task::none()
}
Ok(plugins)
},
|result| {
handle_backend_error(result, |plugins| {
ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded(
plugins,
))
})
},
)
}
PluginTableMsgOut::SelectItem(selected_item) => {
Task::done(ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::SelectItem(
selected_item,
)))
}
PluginTableMsgOut::ToggleShowEntrypoints { plugin_id } => {
Task::done(ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::Noop))
}
}
}
})
}
ManagementAppPluginMsgIn::ToggleShowEntrypoint { plugin_id } => {
let plugins = {
let mut plugin_data = self.plugin_data.borrow_mut();
let settings_plugin_data = plugin_data.plugins_state.get_mut(&plugin_id).unwrap();
settings_plugin_data.show_entrypoints = !settings_plugin_data.show_entrypoints;
plugin_data.plugins.clone()
};
self.apply_plugin_fetch(plugins);
Task::none()
}
ManagementAppPluginMsgIn::PluginPreferenceMsg(msg) => {
match msg {
@ -204,7 +208,11 @@ impl ManagementAppPluginsState {
Ok(())
},
|result| handle_backend_error(result, |()| ManagementAppPluginMsgOut::Noop),
|result| {
handle_backend_error(result, |()| {
ManagementAppPluginMsgOut::Outer(ManagementAppMsg::Noop)
})
},
)
}
}
@ -219,11 +227,13 @@ impl ManagementAppPluginsState {
Ok(plugins)
},
|result| {
handle_backend_error(result, |plugins| ManagementAppPluginMsgOut::PluginsReloaded(plugins))
handle_backend_error(result, |plugins| {
ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded(plugins))
})
},
)
}
ManagementAppPluginMsgIn::PluginsFetched(plugins) => {
ManagementAppPluginMsgIn::PluginsReloaded(plugins) => {
self.apply_plugin_fetch(plugins);
Task::none()
@ -242,12 +252,16 @@ impl ManagementAppPluginsState {
Ok(plugins)
},
|result| {
handle_backend_error(result, |plugins| ManagementAppPluginMsgOut::PluginsReloaded(plugins))
handle_backend_error(result, |plugins| {
ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded(plugins))
})
},
)
}
ManagementAppPluginMsgIn::DownloadPlugin { plugin_id } => {
Task::done(ManagementAppPluginMsgOut::DownloadPlugin { plugin_id })
Task::done(ManagementAppPluginMsgOut::Outer(ManagementAppMsg::DownloadPlugin {
plugin_id,
}))
}
ManagementAppPluginMsgIn::SelectItem(selected_item) => {
self.selected_item = selected_item;
@ -696,6 +710,6 @@ pub fn handle_backend_error<T>(
) -> ManagementAppPluginMsgOut {
match result {
Ok(val) => convert(val),
Err(err) => ManagementAppPluginMsgOut::HandleBackendError(err),
Err(err) => ManagementAppPluginMsgOut::Outer(ManagementAppMsg::HandleBackendError(err)),
}
}

View file

@ -62,11 +62,6 @@ pub struct PluginTableState {
body: Id,
}
pub enum PluginTableUpdateResult {
Command(Task<()>),
Value(PluginTableMsgOut),
}
impl PluginTableState {
pub fn new() -> Self {
Self {
@ -82,22 +77,20 @@ impl PluginTableState {
}
}
pub fn update(&mut self, message: PluginTableMsgIn) -> PluginTableUpdateResult {
pub fn update(&mut self, message: PluginTableMsgIn) -> Task<PluginTableMsgOut> {
match message {
PluginTableMsgIn::TableSyncHeader(offset) => {
PluginTableUpdateResult::Command(scrollable::scroll_to(self.header.clone(), offset))
}
PluginTableMsgIn::TableSyncHeader(offset) => scrollable::scroll_to(self.header.clone(), offset),
PluginTableMsgIn::EnabledToggleItem(item) => {
match item {
EnabledItem::Plugin { enabled, plugin_id } => {
PluginTableUpdateResult::Value(PluginTableMsgOut::SetPluginState { enabled, plugin_id })
Task::done(PluginTableMsgOut::SetPluginState { enabled, plugin_id })
}
EnabledItem::Entrypoint {
enabled,
plugin_id,
entrypoint_id,
} => {
PluginTableUpdateResult::Value(PluginTableMsgOut::SetEntrypointState {
Task::done(PluginTableMsgOut::SetEntrypointState {
enabled,
plugin_id,
entrypoint_id,
@ -105,9 +98,9 @@ impl PluginTableState {
}
}
}
PluginTableMsgIn::SelectItem(item) => PluginTableUpdateResult::Value(PluginTableMsgOut::SelectItem(item)),
PluginTableMsgIn::SelectItem(item) => Task::done(PluginTableMsgOut::SelectItem(item)),
PluginTableMsgIn::ToggleShowEntrypoints { plugin_id } => {
PluginTableUpdateResult::Value(PluginTableMsgOut::ToggleShowEntrypoints { plugin_id })
Task::done(PluginTableMsgOut::ToggleShowEntrypoints { plugin_id })
}
}
}