logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
21
crates/svg_preview/Cargo.toml
Normal file
21
crates/svg_preview/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "svg_preview"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/svg_preview.rs"
|
||||
|
||||
[dependencies]
|
||||
multi_buffer.workspace = true
|
||||
file_icons.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
1
crates/svg_preview/LICENSE-GPL
Symbolic link
1
crates/svg_preview/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
24
crates/svg_preview/src/svg_preview.rs
Normal file
24
crates/svg_preview/src/svg_preview.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use gpui::{App, actions};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub mod svg_preview_view;
|
||||
|
||||
pub use zed_actions::preview::svg::{OpenPreview, OpenPreviewToTheSide};
|
||||
|
||||
actions!(
|
||||
svg,
|
||||
[
|
||||
/// Opens a following SVG preview that syncs with the editor.
|
||||
OpenFollowingPreview
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(|workspace: &mut Workspace, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
};
|
||||
crate::svg_preview_view::SvgPreviewView::register(workspace, window, cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
341
crates/svg_preview/src/svg_preview_view.rs
Normal file
341
crates/svg_preview/src/svg_preview_view.rs
Normal file
@@ -0,0 +1,341 @@
|
||||
use std::mem;
|
||||
use std::sync::Arc;
|
||||
|
||||
use file_icons::FileIcons;
|
||||
use gpui::{
|
||||
App, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render,
|
||||
RenderImage, Styled, Subscription, Task, WeakEntity, Window, div, img,
|
||||
};
|
||||
use language::{Buffer, BufferEvent};
|
||||
use multi_buffer::MultiBuffer;
|
||||
use ui::prelude::*;
|
||||
use workspace::item::Item;
|
||||
use workspace::{Pane, Workspace};
|
||||
|
||||
use crate::{OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide};
|
||||
|
||||
pub struct SvgPreviewView {
|
||||
focus_handle: FocusHandle,
|
||||
buffer: Option<Entity<Buffer>>,
|
||||
current_svg: Option<Result<Arc<RenderImage>, SharedString>>,
|
||||
_refresh: Task<()>,
|
||||
_buffer_subscription: Option<Subscription>,
|
||||
_workspace_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum SvgPreviewMode {
|
||||
/// The preview will always show the contents of the provided editor.
|
||||
Default,
|
||||
/// The preview will "follow" the last active editor of an SVG file.
|
||||
Follow,
|
||||
}
|
||||
|
||||
impl SvgPreviewView {
|
||||
pub fn new(
|
||||
mode: SvgPreviewMode,
|
||||
active_buffer: Entity<MultiBuffer>,
|
||||
workspace_handle: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
let workspace_subscription = if mode == SvgPreviewMode::Follow
|
||||
&& let Some(workspace) = workspace_handle.upgrade()
|
||||
{
|
||||
Some(Self::subscribe_to_workspace(workspace, window, cx))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let buffer = active_buffer.read_with(cx, |buffer, _cx| buffer.as_singleton());
|
||||
|
||||
let subscription = buffer
|
||||
.as_ref()
|
||||
.map(|buffer| Self::create_buffer_subscription(buffer, window, cx));
|
||||
|
||||
let mut this = Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
buffer,
|
||||
current_svg: None,
|
||||
_buffer_subscription: subscription,
|
||||
_workspace_subscription: workspace_subscription,
|
||||
_refresh: Task::ready(()),
|
||||
};
|
||||
this.render_image(window, cx);
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
fn subscribe_to_workspace(
|
||||
workspace: Entity<Workspace>,
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Subscription {
|
||||
cx.subscribe_in(
|
||||
&workspace,
|
||||
window,
|
||||
move |this: &mut SvgPreviewView, workspace, event: &workspace::Event, window, cx| {
|
||||
if let workspace::Event::ActiveItemChanged = event {
|
||||
let workspace = workspace.read(cx);
|
||||
if let Some(active_item) = workspace.active_item(cx)
|
||||
&& let Some(buffer) = active_item.downcast::<MultiBuffer>()
|
||||
&& Self::is_svg_file(&buffer, cx)
|
||||
{
|
||||
let Some(buffer) = buffer.read(cx).as_singleton() else {
|
||||
return;
|
||||
};
|
||||
if this.buffer.as_ref() != Some(&buffer) {
|
||||
this._buffer_subscription =
|
||||
Some(Self::create_buffer_subscription(&buffer, window, cx));
|
||||
this.buffer = Some(buffer);
|
||||
this.render_image(window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
} else {
|
||||
this.set_current(None, window, cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_image(&mut self, window: &Window, cx: &mut Context<Self>) {
|
||||
let Some(buffer) = self.buffer.as_ref() else {
|
||||
return;
|
||||
};
|
||||
const SCALE_FACTOR: f32 = 1.0;
|
||||
|
||||
let renderer = cx.svg_renderer();
|
||||
let content = buffer.read(cx).snapshot();
|
||||
let background_task = cx.background_spawn(async move {
|
||||
renderer.render_single_frame(content.text().as_bytes(), SCALE_FACTOR)
|
||||
});
|
||||
|
||||
self._refresh = cx.spawn_in(window, async move |this, cx| {
|
||||
let result = background_task.await;
|
||||
|
||||
this.update_in(cx, |view, window, cx| {
|
||||
let current = result.map_err(|e| e.to_string().into());
|
||||
view.set_current(Some(current), window, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
|
||||
fn set_current(
|
||||
&mut self,
|
||||
image: Option<Result<Arc<RenderImage>, SharedString>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(Ok(image)) = mem::replace(&mut self.current_svg, image) {
|
||||
window.drop_image(image).ok();
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn find_existing_preview_item_idx(
|
||||
pane: &Pane,
|
||||
buffer: &Entity<MultiBuffer>,
|
||||
cx: &App,
|
||||
) -> Option<usize> {
|
||||
let buffer_id = buffer.read(cx).as_singleton()?.entity_id();
|
||||
pane.items_of_type::<SvgPreviewView>()
|
||||
.find(|view| {
|
||||
view.read(cx)
|
||||
.buffer
|
||||
.as_ref()
|
||||
.is_some_and(|buffer| buffer.entity_id() == buffer_id)
|
||||
})
|
||||
.and_then(|view| pane.index_for_item(&view))
|
||||
}
|
||||
|
||||
pub fn resolve_active_item_as_svg_buffer(
|
||||
workspace: &Workspace,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Option<Entity<MultiBuffer>> {
|
||||
workspace
|
||||
.active_item(cx)?
|
||||
.act_as::<MultiBuffer>(cx)
|
||||
.filter(|buffer| Self::is_svg_file(&buffer, cx))
|
||||
}
|
||||
|
||||
fn create_svg_view(
|
||||
mode: SvgPreviewMode,
|
||||
workspace: &mut Workspace,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Entity<SvgPreviewView> {
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
SvgPreviewView::new(mode, buffer, workspace_handle, window, cx)
|
||||
}
|
||||
|
||||
fn create_buffer_subscription(
|
||||
buffer: &Entity<Buffer>,
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Subscription {
|
||||
cx.subscribe_in(
|
||||
buffer,
|
||||
window,
|
||||
move |this, _buffer, event: &BufferEvent, window, cx| match event {
|
||||
BufferEvent::Edited { .. } | BufferEvent::Saved => {
|
||||
this.render_image(window, cx);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_svg_file(buffer: &Entity<MultiBuffer>, cx: &App) -> bool {
|
||||
buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).file())
|
||||
.is_some_and(|file| {
|
||||
std::path::Path::new(file.file_name(cx))
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("svg"))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>) {
|
||||
workspace.register_action(move |workspace, _: &OpenPreview, window, cx| {
|
||||
if let Some(buffer) = Self::resolve_active_item_as_svg_buffer(workspace, cx)
|
||||
&& Self::is_svg_file(&buffer, cx)
|
||||
{
|
||||
let view = Self::create_svg_view(
|
||||
SvgPreviewMode::Default,
|
||||
workspace,
|
||||
buffer.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
if let Some(existing_view_idx) =
|
||||
Self::find_existing_preview_item_idx(pane, &buffer, cx)
|
||||
{
|
||||
pane.activate_item(existing_view_idx, true, true, window, cx);
|
||||
} else {
|
||||
pane.add_item(Box::new(view), true, true, None, window, cx)
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| {
|
||||
if let Some(editor) = Self::resolve_active_item_as_svg_buffer(workspace, cx)
|
||||
&& Self::is_svg_file(&editor, cx)
|
||||
{
|
||||
let editor_clone = editor.clone();
|
||||
let view = Self::create_svg_view(
|
||||
SvgPreviewMode::Default,
|
||||
workspace,
|
||||
editor_clone,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let pane = workspace
|
||||
.find_pane_in_direction(workspace::SplitDirection::Right, cx)
|
||||
.unwrap_or_else(|| {
|
||||
workspace.split_pane(
|
||||
workspace.active_pane().clone(),
|
||||
workspace::SplitDirection::Right,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
pane.update(cx, |pane, cx| {
|
||||
if let Some(existing_view_idx) =
|
||||
Self::find_existing_preview_item_idx(pane, &editor, cx)
|
||||
{
|
||||
pane.activate_item(existing_view_idx, true, true, window, cx);
|
||||
} else {
|
||||
pane.add_item(Box::new(view), false, false, None, window, cx)
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| {
|
||||
if let Some(editor) = Self::resolve_active_item_as_svg_buffer(workspace, cx)
|
||||
&& Self::is_svg_file(&editor, cx)
|
||||
{
|
||||
let view =
|
||||
Self::create_svg_view(SvgPreviewMode::Follow, workspace, editor, window, cx);
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(Box::new(view), true, true, None, window, cx)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SvgPreviewView {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.id("SvgPreview")
|
||||
.key_context("SvgPreview")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.map(|this| match self.current_svg.clone() {
|
||||
Some(Ok(image)) => {
|
||||
this.child(img(image).max_w_full().max_h_full().with_fallback(|| {
|
||||
h_flex()
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::Warning))
|
||||
.child("Failed to load SVG image")
|
||||
.into_any_element()
|
||||
}))
|
||||
}
|
||||
Some(Err(e)) => this.child(div().p_4().child(e).into_any_element()),
|
||||
None => this.child(div().p_4().child("No SVG file selected")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for SvgPreviewView {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for SvgPreviewView {}
|
||||
|
||||
impl Item for SvgPreviewView {
|
||||
type Event = ();
|
||||
|
||||
fn tab_icon(&self, _window: &Window, cx: &App) -> Option<Icon> {
|
||||
self.buffer
|
||||
.as_ref()
|
||||
.and_then(|buffer| buffer.read(cx).file())
|
||||
.and_then(|file| FileIcons::get_icon(file.path().as_std_path(), cx))
|
||||
.map(Icon::from_path)
|
||||
.or_else(|| Some(Icon::new(IconName::Image)))
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
|
||||
self.buffer
|
||||
.as_ref()
|
||||
.and_then(|svg_path| svg_path.read(cx).file())
|
||||
.map(|name| format!("Preview {}", name.file_name(cx)).into())
|
||||
.unwrap_or_else(|| "SVG Preview".into())
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("svg preview: open")
|
||||
}
|
||||
|
||||
fn to_item_events(_event: &Self::Event, _f: &mut dyn FnMut(workspace::item::ItemEvent)) {}
|
||||
}
|
||||
Reference in New Issue
Block a user