diff --git a/crates/component-zoo/src/app.rs b/crates/component-zoo/src/app.rs index 279cee7..0b25315 100644 --- a/crates/component-zoo/src/app.rs +++ b/crates/component-zoo/src/app.rs @@ -45,6 +45,7 @@ live_design! { use makepad_component::widgets::popover::*; use makepad_component::widgets::label::*; use makepad_component::widgets::text::*; + use makepad_component::widgets::link::*; use makepad_component::widgets::alert::*; // ============================================================ @@ -998,6 +999,68 @@ live_design! { {} + // ===== Link Section ===== + { + width: Fill, height: Fit, + flow: Down, + spacing: 16, + + { text: "Link" } + + // Basic links + { + width: Fit, height: Fit, + flow: Down, + spacing: 8, + + { text: "Basic Links" } + + { + width: Fit, height: Fit, + flow: Right, + spacing: 16, + + { + text: "Visit Makepad" + href: "https://makepad.nl" + } + { + text: "GitHub Repository" + href: "https://github.com/makepad/makepad" + } + { + text: "Documentation" + href: "https://makepad.dev" + } + } + } + + // Inline with text + { + width: Fit, height: Fit, + flow: Down, + spacing: 8, + + { text: "Inline with Text" } + + { + width: Fit, height: Fit, + flow: Right, + spacing: 4, + align: { y: 0.5 } + + { text: "Check out" } + { + text: "our website" + href: "https://makepad.nl" + } + { text: "for more information." } + } + } + } + + {} + // ===== Badge Section ===== { width: Fill, height: Fit, diff --git a/crates/ui/src/widgets/link.rs b/crates/ui/src/widgets/link.rs new file mode 100644 index 0000000..8063752 --- /dev/null +++ b/crates/ui/src/widgets/link.rs @@ -0,0 +1,226 @@ +use makepad_widgets::*; + +live_design! { + use link::theme::*; + use link::shaders::*; + use link::widgets::*; + + use crate::theme::colors::*; + + pub MpLink = {{MpLink}} { + width: Fit, + height: Fit, + + draw_text: { + text_style: { font_size: 14.0 } + color: (PRIMARY) + + instance hover: 0.0 + instance pressed: 0.0 + + fn get_color(self) -> vec4 { + return mix( + mix(self.color, self.color * 0.8, self.hover), + self.color * 0.6, + self.pressed + ); + } + } + + draw_underline: { + instance color: (PRIMARY) + instance hover: 0.0 + instance pressed: 0.0 + + fn pixel(self) -> vec4 { + let sdf = Sdf2d::viewport(self.pos * self.rect_size); + let y = self.rect_size.y - 1.0; + sdf.move_to(0.0, y); + sdf.line_to(self.rect_size.x, y); + + let line_color = mix( + mix(self.color, self.color * 0.8, self.hover), + self.color * 0.6, + self.pressed + ); + + return sdf.stroke(line_color, 1.0); + } + } + + cursor: Hand + + animator: { + hover = { + default: off + off = { + from: { all: Forward { duration: 0.15 } } + apply: { + draw_text: { hover: 0.0 } + draw_underline: { hover: 0.0 } + } + } + on = { + from: { all: Forward { duration: 0.15 } } + apply: { + draw_text: { hover: 1.0 } + draw_underline: { hover: 1.0 } + } + } + } + pressed = { + default: off + off = { + from: { all: Forward { duration: 0.1 } } + apply: { + draw_text: { pressed: 0.0 } + draw_underline: { pressed: 0.0 } + } + } + on = { + from: { all: Forward { duration: 0.1 } } + apply: { + draw_text: { pressed: 1.0 } + draw_underline: { pressed: 1.0 } + } + } + } + } + + text: "" + } +} + +#[derive(Live, LiveHook, Widget)] +pub struct MpLink { + #[redraw] + #[live] + draw_text: DrawText, + + #[live] + draw_underline: DrawQuad, + + #[walk] + walk: Walk, + + #[layout] + layout: Layout, + + #[live] + text: ArcStringMut, + + #[live] + href: ArcStringMut, + + #[live(false)] + disabled: bool, + + #[animator] + animator: Animator, + + #[rust] + area: Area, +} + +#[derive(Clone, Debug, DefaultNone)] +pub enum MpLinkAction { + Clicked, + None, +} + +impl Widget for MpLink { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let uid = self.widget_uid(); + + if self.animator_handle_event(cx, event).must_redraw() { + self.redraw(cx); + } + + if self.disabled { + return; + } + + match event.hits(cx, self.area) { + Hit::FingerHoverIn(_) => { + cx.set_cursor(MouseCursor::Hand); + self.animator_play(cx, ids!(hover.on)); + } + Hit::FingerHoverOut(_) => { + cx.set_cursor(MouseCursor::Default); + self.animator_play(cx, ids!(hover.off)); + } + Hit::FingerDown(_) => { + self.animator_play(cx, ids!(pressed.on)); + } + Hit::FingerUp(fe) => { + self.animator_play(cx, ids!(pressed.off)); + if fe.is_over { + self.activate_link(cx, uid, scope); + } + } + Hit::KeyDown(ke) => { + if ke.key_code == KeyCode::ReturnKey || ke.key_code == KeyCode::Space { + self.animator_play(cx, ids!(pressed.on)); + self.activate_link(cx, uid, scope); + } + } + _ => {} + } + } + + fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep { + self.draw_underline.begin(cx, walk, self.layout); + self.draw_text.draw_walk(cx, Walk::fit(), Align::default(), self.text.as_ref()); + self.draw_underline.end(cx); + self.area = self.draw_underline.area(); + DrawStep::done() + } +} + +impl MpLink { + fn activate_link(&mut self, cx: &mut Cx, uid: WidgetUid, scope: &Scope) { + let href = self.href.as_ref(); + if !href.is_empty() { + cx.open_url(href, OpenUrlInPlace::No); + } + cx.widget_action(uid, &scope.path, MpLinkAction::Clicked); + } + + pub fn clicked(&self, actions: &Actions) -> bool { + if let Some(action) = actions.find_widget_action(self.widget_uid()) { + matches!(action.cast::(), MpLinkAction::Clicked) + } else { + false + } + } + + pub fn set_text(&mut self, text: &str) { + self.text.as_mut_empty().push_str(text); + } + + pub fn set_href(&mut self, href: &str) { + self.href.as_mut_empty().push_str(href); + } +} + +impl MpLinkRef { + pub fn clicked(&self, actions: &Actions) -> bool { + if let Some(inner) = self.borrow() { + inner.clicked(actions) + } else { + false + } + } + + pub fn set_text(&self, text: &str) { + if let Some(mut inner) = self.borrow_mut() { + inner.set_text(text); + } + } + + pub fn set_href(&self, href: &str) { + if let Some(mut inner) = self.borrow_mut() { + inner.set_href(href); + } + } +} diff --git a/crates/ui/src/widgets/mod.rs b/crates/ui/src/widgets/mod.rs index 0f8adf3..dcb667d 100644 --- a/crates/ui/src/widgets/mod.rs +++ b/crates/ui/src/widgets/mod.rs @@ -9,6 +9,7 @@ pub mod divider; pub mod dropdown; pub mod input; pub mod label; +pub mod link; pub mod list; pub mod modal; pub mod notification; @@ -32,6 +33,7 @@ pub use checkbox::*; pub use divider::*; pub use input::*; pub use label::*; +pub use link::*; pub use page_flip::*; pub use progress::*; pub use radio::*; @@ -63,6 +65,7 @@ pub fn live_design(cx: &mut Cx) { crate::widgets::dropdown::live_design(cx); crate::widgets::input::live_design(cx); crate::widgets::label::live_design(cx); + crate::widgets::link::live_design(cx); crate::widgets::list::live_design(cx); crate::widgets::modal::live_design(cx); crate::widgets::notification::live_design(cx);