Files
2026-07-13 12:08:36 +08:00

1024 lines
37 KiB
Rust

use std::{
collections::HashSet,
rc::Rc,
sync::{Arc, atomic::AtomicU64},
};
use anyhow::Result;
use floem::{
IntoView, View,
action::show_context_menu,
ext_event::create_ext_action,
keyboard::Modifiers,
kurbo::Rect,
menu::{Menu, MenuItem},
reactive::{
RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, create_effect,
create_memo, create_rw_signal, use_context,
},
style::CursorStyle,
views::{
Decorators, container, dyn_container, dyn_stack, empty, img, label,
rich_text, scroll, stack, svg, text,
},
};
use indexmap::IndexMap;
use lapce_core::{command::EditCommand, directory::Directory, mode::Mode};
use lapce_proxy::plugin::{download_volt, volt_icon, wasi::find_all_volts};
use lapce_rpc::{
core::{CoreNotification, CoreRpcHandler},
plugin::{VoltID, VoltInfo, VoltMetadata},
};
use lsp_types::MessageType;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::{
command::{CommandExecuted, CommandKind},
config::{LapceConfig, color::LapceColor},
db::LapceDb,
editor::EditorData,
keypress::{KeyPressFocus, condition::Condition},
main_split::Editors,
markdown::{MarkdownContent, parse_markdown},
panel::plugin_view::VOLT_DEFAULT_PNG,
web_link::web_link,
window_tab::CommonData,
};
type PluginInfo = Option<(
Option<VoltMetadata>,
VoltInfo,
Option<VoltIcon>,
Option<VoltInfo>,
Option<RwSignal<bool>>,
)>;
#[derive(Clone, PartialEq, Eq)]
pub enum VoltIcon {
Svg(String),
Img(Vec<u8>),
}
impl VoltIcon {
pub fn from_bytes(buf: &[u8]) -> Result<Self> {
if let Ok(s) = std::str::from_utf8(buf) {
Ok(VoltIcon::Svg(s.to_string()))
} else {
Ok(VoltIcon::Img(buf.to_vec()))
}
}
}
#[derive(Deserialize, Serialize)]
pub struct VoltsInfo {
pub plugins: Vec<VoltInfo>,
pub total: usize,
}
#[derive(Clone)]
pub struct InstalledVoltData {
pub meta: RwSignal<VoltMetadata>,
pub icon: RwSignal<Option<VoltIcon>>,
pub latest: RwSignal<VoltInfo>,
}
#[derive(Clone, PartialEq)]
pub struct AvailableVoltData {
pub info: RwSignal<VoltInfo>,
pub icon: RwSignal<Option<VoltIcon>>,
pub installing: RwSignal<bool>,
}
#[derive(Clone, Debug)]
pub struct AvailableVoltList {
pub loading: RwSignal<bool>,
pub query_id: RwSignal<usize>,
pub query_editor: EditorData,
pub volts: RwSignal<IndexMap<VoltID, AvailableVoltData>>,
pub total: RwSignal<usize>,
}
#[derive(Clone, Debug)]
pub struct PluginData {
pub installed: RwSignal<IndexMap<VoltID, InstalledVoltData>>,
pub available: AvailableVoltList,
pub all: RwSignal<im::HashMap<VoltID, AvailableVoltData>>,
pub disabled: RwSignal<HashSet<VoltID>>,
pub workspace_disabled: RwSignal<HashSet<VoltID>>,
pub common: Rc<CommonData>,
}
impl KeyPressFocus for PluginData {
fn get_mode(&self) -> Mode {
Mode::Insert
}
fn check_condition(&self, condition: Condition) -> bool {
matches!(condition, Condition::PanelFocus)
}
fn run_command(
&self,
command: &crate::command::LapceCommand,
count: Option<usize>,
mods: Modifiers,
) -> CommandExecuted {
match &command.kind {
CommandKind::Workbench(_) => {}
CommandKind::Scroll(_) => {}
CommandKind::Focus(_) => {}
CommandKind::Edit(_)
| CommandKind::Move(_)
| CommandKind::MultiSelection(_) => {
#[allow(clippy::single_match)]
match command.kind {
CommandKind::Edit(EditCommand::InsertNewLine) => {
return CommandExecuted::Yes;
}
_ => {}
}
return self
.available
.query_editor
.run_command(command, count, mods);
}
CommandKind::MotionMode(_) => {}
}
CommandExecuted::No
}
fn receive_char(&self, c: &str) {
self.available.query_editor.receive_char(c);
}
}
impl PluginData {
pub fn new(
cx: Scope,
disabled: HashSet<VoltID>,
workspace_disabled: HashSet<VoltID>,
editors: Editors,
common: Rc<CommonData>,
core_rpc: CoreRpcHandler,
) -> Self {
let installed = cx.create_rw_signal(IndexMap::new());
let available = AvailableVoltList {
loading: cx.create_rw_signal(false),
volts: cx.create_rw_signal(IndexMap::new()),
total: cx.create_rw_signal(0),
query_id: cx.create_rw_signal(0),
query_editor: editors.make_local(cx, common.clone()),
};
let disabled = cx.create_rw_signal(disabled);
let workspace_disabled = cx.create_rw_signal(workspace_disabled);
let plugin = Self {
installed,
available,
all: cx.create_rw_signal(im::HashMap::new()),
disabled,
workspace_disabled,
common,
};
plugin.load_available_volts("", 0, core_rpc.clone());
{
let plugin = plugin.clone();
let extra_plugin_paths =
plugin.common.window_common.extra_plugin_paths.clone();
let send = create_ext_action(
cx,
move |volts: Vec<(Option<Vec<u8>>, VoltMetadata)>| {
for (icon, meta) in volts {
plugin.volt_installed(&meta, &icon);
}
},
);
std::thread::Builder::new()
.name("FindAllVolts".to_owned())
.spawn(move || {
let volts = find_all_volts(&extra_plugin_paths);
let volts = volts
.into_iter()
.filter_map(|meta| {
if meta.wasm.is_none() {
Some((volt_icon(&meta), meta))
} else {
None
}
})
.collect::<Vec<_>>();
send(volts);
})
.unwrap();
}
{
let plugin = plugin.clone();
cx.create_effect(move |s| {
let query = plugin
.available
.query_editor
.doc_signal()
.get()
.buffer
.with(|buffer| buffer.to_string());
if s.as_ref() == Some(&query) {
return query;
}
plugin.available.query_id.update(|id| *id += 1);
plugin.available.loading.set(false);
plugin.available.volts.update(|v| v.clear());
plugin.load_available_volts(&query, 0, core_rpc.clone());
query
});
}
plugin
}
pub fn volt_installed(&self, volt: &VoltMetadata, icon: &Option<Vec<u8>>) {
let volt_id = volt.id();
let (existing, is_latest, volt_data) = self
.installed
.try_update(|installed| {
if let Some(v) = installed.get(&volt_id) {
(true, true, v.to_owned())
} else {
let (info, is_latest) = if let Some(volt) = self
.available
.volts
.with_untracked(|all| all.get(&volt_id).cloned())
{
(volt.info.get_untracked(), true)
} else {
(volt.info(), false)
};
let latest = self.common.scope.create_rw_signal(info);
let data = InstalledVoltData {
meta: self.common.scope.create_rw_signal(volt.clone()),
icon: self.common.scope.create_rw_signal(
icon.as_ref()
.and_then(|icon| VoltIcon::from_bytes(icon).ok()),
),
latest,
};
installed.insert(volt_id, data.clone());
(false, is_latest, data)
}
})
.unwrap();
if existing {
volt_data.meta.set(volt.clone());
volt_data.icon.set(
icon.as_ref()
.and_then(|icon| VoltIcon::from_bytes(icon).ok()),
);
}
let latest = volt_data.latest;
if !is_latest {
let url = format!(
"https://plugins.lapce.dev/api/v1/plugins/{}/{}/latest",
volt.author, volt.name
);
let send = create_ext_action(self.common.scope, move |info| {
if let Some(info) = info {
latest.set(info);
}
});
std::thread::spawn(move || {
let info: Option<VoltInfo> = lapce_proxy::get_url(url, None)
.ok()
.and_then(|r| r.json().ok());
send(info);
});
}
}
pub fn volt_removed(&self, volt: &VoltInfo) {
let id = volt.id();
self.installed.update(|installed| {
installed.swap_remove(&id);
});
if self.disabled.with_untracked(|d| d.contains(&id)) {
self.disabled.update(|d| {
d.remove(&id);
});
let db: Arc<LapceDb> = use_context().unwrap();
db.save_disabled_volts(
self.disabled.get_untracked().into_iter().collect(),
);
}
if self.workspace_disabled.with_untracked(|d| d.contains(&id)) {
self.workspace_disabled.update(|d| {
d.remove(&id);
});
let db: Arc<LapceDb> = use_context().unwrap();
db.save_workspace_disabled_volts(
self.common.workspace.clone(),
self.workspace_disabled
.get_untracked()
.into_iter()
.collect(),
);
}
}
fn load_available_volts(
&self,
query: &str,
offset: usize,
core_rpc: CoreRpcHandler,
) {
if self.available.loading.get_untracked() {
return;
}
self.available.loading.set(true);
let volts = self.available.volts;
let volts_total = self.available.total;
let cx = self.common.scope;
let loading = self.available.loading;
let query_id = self.available.query_id;
let current_query_id = self.available.query_id.get_untracked();
let all = self.all;
let send =
create_ext_action(self.common.scope, move |new: Result<VoltsInfo>| {
loading.set(false);
if query_id.get_untracked() != current_query_id {
return;
}
match new {
Ok(new) => {
volts.update(|volts| {
volts.extend(new.plugins.into_iter().map(|volt| {
let icon = cx.create_rw_signal(None);
let send = create_ext_action(cx, move |result| {
if let Ok(i) = result {
icon.set(Some(i));
}
});
{
let volt = volt.clone();
std::thread::spawn(move || {
let result = Self::load_icon(&volt);
send(result);
});
}
let data = AvailableVoltData {
info: cx.create_rw_signal(volt.clone()),
icon,
installing: cx.create_rw_signal(false),
};
all.update(|all| {
all.insert(volt.id(), data.clone());
});
(volt.id(), data)
}));
});
volts_total.set(new.total);
}
Err(err) => {
tracing::error!("{:?}", err);
core_rpc.notification(CoreNotification::ShowMessage {
title: "Request Available Plugins".to_string(),
message: lsp_types::ShowMessageParams {
typ: MessageType::ERROR,
message: err.to_string(),
},
});
}
}
});
let query = query.to_string();
std::thread::spawn(move || {
let volts = Self::query_volts(&query, offset);
send(volts);
});
}
fn load_icon(volt: &VoltInfo) -> Result<VoltIcon> {
let url = format!(
"https://plugins.lapce.dev/api/v1/plugins/{}/{}/{}/icon?id={}",
volt.author, volt.name, volt.version, volt.updated_at_ts
);
let cache_file_path = Directory::cache_directory().map(|cache_dir| {
let mut hasher = Sha256::new();
hasher.update(url.as_bytes());
let filename = format!("{:x}", hasher.finalize());
cache_dir.join(filename)
});
let cache_content =
cache_file_path.as_ref().and_then(|p| std::fs::read(p).ok());
let content = match cache_content {
Some(content) => content,
None => {
let resp = lapce_proxy::get_url(&url, None)?;
if !resp.status().is_success() {
return Err(anyhow::anyhow!("can't download icon"));
}
let buf = resp.bytes()?.to_vec();
if let Some(path) = cache_file_path.as_ref() {
if let Err(err) = std::fs::write(path, &buf) {
tracing::error!("{:?}", err);
}
}
buf
}
};
VoltIcon::from_bytes(&content)
}
fn download_readme(
volt: &VoltInfo,
config: &LapceConfig,
) -> Result<Vec<MarkdownContent>> {
let url = format!(
"https://plugins.lapce.dev/api/v1/plugins/{}/{}/{}/readme",
volt.author, volt.name, volt.version
);
let resp = lapce_proxy::get_url(&url, None)?;
if resp.status() != 200 {
let text = parse_markdown("Plugin doesn't have a README", 2.0, config);
return Ok(text);
}
let text = resp.text()?;
let text = parse_markdown(&text, 2.0, config);
Ok(text)
}
fn query_volts(query: &str, offset: usize) -> Result<VoltsInfo> {
let url = format!(
"https://plugins.lapce.dev/api/v1/plugins?q={query}&offset={offset}"
);
let plugins: VoltsInfo = lapce_proxy::get_url(url, None)?.json()?;
Ok(plugins)
}
fn all_loaded(&self) -> bool {
self.available.volts.with_untracked(|v| v.len())
>= self.available.total.get_untracked()
}
pub fn load_more_available(&self, core_rpc: CoreRpcHandler) {
if self.all_loaded() {
return;
}
let query = self
.available
.query_editor
.doc()
.buffer
.with_untracked(|buffer| buffer.to_string());
let offset = self.available.volts.with_untracked(|v| v.len());
self.load_available_volts(&query, offset, core_rpc);
}
pub fn install_volt(&self, info: VoltInfo) {
self.available.volts.with_untracked(|volts| {
if let Some(volt) = volts.get(&info.id()) {
volt.installing.set(true);
};
});
if info.wasm {
self.common.proxy.install_volt(info);
} else {
let plugin = self.clone();
let send = create_ext_action(self.common.scope, move |result| {
if let Ok((meta, icon)) = result {
plugin.volt_installed(&meta, &icon);
}
});
std::thread::spawn(move || {
let download = || -> Result<(VoltMetadata, Option<Vec<u8>>)> {
let download_volt_result = download_volt(&info);
let meta = download_volt_result?;
let icon = volt_icon(&meta);
Ok((meta, icon))
};
send(download());
});
}
}
pub fn plugin_disabled(&self, id: &VoltID) -> bool {
self.disabled.with_untracked(|d| d.contains(id))
|| self.workspace_disabled.with_untracked(|d| d.contains(id))
}
pub fn enable_volt(&self, volt: VoltInfo) {
let id = volt.id();
self.disabled.update(|d| {
d.remove(&id);
});
if !self.plugin_disabled(&id) {
self.common.proxy.enable_volt(volt);
}
let db: Arc<LapceDb> = use_context().unwrap();
db.save_disabled_volts(self.disabled.get_untracked().into_iter().collect());
}
pub fn disable_volt(&self, volt: VoltInfo) {
let id = volt.id();
self.disabled.update(|d| {
d.insert(id);
});
self.common.proxy.disable_volt(volt);
let db: Arc<LapceDb> = use_context().unwrap();
db.save_disabled_volts(self.disabled.get_untracked().into_iter().collect());
}
pub fn enable_volt_for_ws(&self, volt: VoltInfo) {
let id = volt.id();
self.workspace_disabled.update(|d| {
d.remove(&id);
});
if !self.plugin_disabled(&id) {
self.common.proxy.enable_volt(volt);
}
let db: Arc<LapceDb> = use_context().unwrap();
db.save_workspace_disabled_volts(
self.common.workspace.clone(),
self.disabled.get_untracked().into_iter().collect(),
);
}
pub fn disable_volt_for_ws(&self, volt: VoltInfo) {
let id = volt.id();
self.workspace_disabled.update(|d| {
d.insert(id);
});
self.common.proxy.disable_volt(volt);
let db: Arc<LapceDb> = use_context().unwrap();
db.save_workspace_disabled_volts(
self.common.workspace.clone(),
self.disabled.get_untracked().into_iter().collect(),
);
}
pub fn uninstall_volt(&self, volt: VoltMetadata) {
if volt.wasm.is_some() {
self.common.proxy.remove_volt(volt);
} else {
let plugin = self.clone();
let info = volt.info();
let send =
create_ext_action(self.common.scope, move |result: Result<()>| {
if let Ok(()) = result {
plugin.volt_removed(&info);
}
});
std::thread::spawn(move || {
let uninstall = || -> Result<()> {
let path = volt
.dir
.as_ref()
.ok_or_else(|| anyhow::anyhow!("don't have dir"))?;
std::fs::remove_dir_all(path)?;
Ok(())
};
send(uninstall());
});
}
}
pub fn reload_volt(&self, volt: VoltMetadata) {
self.common.proxy.reload_volt(volt);
}
pub fn plugin_controls(&self, meta: VoltMetadata, latest: VoltInfo) -> Menu {
let volt_id = meta.id();
let mut menu = Menu::new("");
if meta.version != latest.version {
menu = menu
.entry(MenuItem::new("Upgrade Plugin").action({
let plugin = self.clone();
let info = latest.clone();
move || {
plugin.install_volt(info.clone());
}
}))
.separator();
}
menu = menu
.entry(MenuItem::new("Reload Plugin").action({
let plugin = self.clone();
let meta = meta.clone();
move || {
plugin.reload_volt(meta.clone());
}
}))
.separator()
.entry(
MenuItem::new("Enable")
.enabled(
self.disabled
.with_untracked(|disabled| disabled.contains(&volt_id)),
)
.action({
let plugin = self.clone();
let volt = meta.info();
move || {
plugin.enable_volt(volt.clone());
}
}),
)
.entry(
MenuItem::new("Disable")
.enabled(
self.disabled
.with_untracked(|disabled| !disabled.contains(&volt_id)),
)
.action({
let plugin = self.clone();
let volt = meta.info();
move || {
plugin.disable_volt(volt.clone());
}
}),
)
.separator()
.entry(
MenuItem::new("Enable For Workspace")
.enabled(
self.workspace_disabled
.with_untracked(|disabled| disabled.contains(&volt_id)),
)
.action({
let plugin = self.clone();
let volt = meta.info();
move || {
plugin.enable_volt_for_ws(volt.clone());
}
}),
)
.entry(
MenuItem::new("Disable For Workspace")
.enabled(
self.workspace_disabled
.with_untracked(|disabled| !disabled.contains(&volt_id)),
)
.action({
let plugin = self.clone();
let volt = meta.info();
move || {
plugin.disable_volt_for_ws(volt.clone());
}
}),
)
.separator()
.entry(MenuItem::new("Uninstall").action({
let plugin = self.clone();
move || {
plugin.uninstall_volt(meta.clone());
}
}));
menu
}
}
pub fn plugin_info_view(plugin: PluginData, volt: VoltID) -> impl View {
let config = plugin.common.config;
let header_rect = create_rw_signal(Rect::ZERO);
let scroll_width: RwSignal<f64> = create_rw_signal(0.0);
let internal_command = plugin.common.internal_command;
let local_plugin = plugin.clone();
let plugin_info = create_memo(move |_| {
plugin
.installed
.with(|volts| {
volts.get(&volt).map(|v| {
(
Some(v.meta.get()),
v.meta.get().info(),
v.icon.get(),
Some(v.latest.get()),
None,
)
})
})
.or_else(|| {
plugin.all.with(|volts| {
volts.get(&volt).map(|v| {
(None, v.info.get(), v.icon.get(), None, Some(v.installing))
})
})
})
});
let version_view = move |plugin: PluginData, plugin_info: PluginInfo| {
let version_info = plugin_info.as_ref().map(|(_, volt, _, latest, _)| {
(
volt.version.clone(),
latest.as_ref().map(|i| i.version.clone()),
)
});
let installing = plugin_info
.as_ref()
.and_then(|(_, _, _, _, installing)| *installing);
let local_version_info = version_info.clone();
let control = {
move |version_info: Option<(String, Option<String>)>| match version_info
.as_ref()
.map(|(v, l)| match l {
Some(l) => (true, l == v),
None => (false, false),
}) {
Some((true, true)) => "Installed ▼",
Some((true, false)) => "Upgrade ▼",
_ => {
if installing.map(|i| i.get()).unwrap_or(false) {
"Installing"
} else {
"Install"
}
}
}
};
let local_plugin_info = plugin_info.clone();
let local_plugin = plugin.clone();
stack((
text(
version_info
.as_ref()
.map(|(v, _)| format!("v{v}"))
.unwrap_or_default(),
),
label(move || control(local_version_info.clone()))
.style(move |s| {
let config = config.get();
s.margin_left(10)
.padding_horiz(10)
.border_radius(6.0)
.color(
config
.color(LapceColor::LAPCE_BUTTON_PRIMARY_FOREGROUND),
)
.background(
config
.color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND),
)
.hover(|s| {
s.cursor(CursorStyle::Pointer).background(
config
.color(
LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND,
)
.multiply_alpha(0.8),
)
})
.active(|s| {
s.background(
config
.color(
LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND,
)
.multiply_alpha(0.6),
)
})
.disabled(|s| {
s.background(config.color(LapceColor::EDITOR_DIM))
})
.selectable(false)
})
.disabled(move || installing.map(|i| i.get()).unwrap_or(false))
.on_click_stop(move |_| {
if let Some((meta, info, _, latest, _)) =
local_plugin_info.as_ref()
{
if let Some(meta) = meta {
let menu = local_plugin.plugin_controls(
meta.to_owned(),
latest.clone().unwrap_or_else(|| info.to_owned()),
);
show_context_menu(menu, None);
} else {
local_plugin.install_volt(info.to_owned());
}
}
}),
))
};
scroll(
dyn_container(
move || plugin_info.get(),
move |plugin_info| {
stack((
stack((
match plugin_info
.as_ref()
.and_then(|(_, _, icon, _, _)| icon.clone())
{
None => container(
img(move || VOLT_DEFAULT_PNG.to_vec())
.style(|s| s.size_full()),
),
Some(VoltIcon::Svg(svg_str)) => container(
svg(move || svg_str.clone())
.style(|s| s.size_full()),
),
Some(VoltIcon::Img(buf)) => container(
img(move || buf.clone()).style(|s| s.size_full()),
),
}
.style(|s| {
s.min_size(150.0, 150.0).size(150.0, 150.0).padding(20)
}),
stack((
text(
plugin_info
.as_ref()
.map(|(_, volt, _, _, _)| {
volt.display_name.as_str()
})
.unwrap_or(""),
)
.style(move |s| {
s.font_bold().font_size(
(config.get().ui.font_size() as f32 * 1.6)
.round(),
)
}),
text(
plugin_info
.as_ref()
.map(|(_, volt, _, _, _)| {
volt.description.as_str()
})
.unwrap_or(""),
)
.style(move |s| {
let scroll_width = scroll_width.get();
s.max_width(
scroll_width
.clamp(200.0 + 60.0 * 2.0 + 200.0, 800.0)
- 60.0 * 2.0
- 200.0,
)
}),
{
let repo = plugin_info
.as_ref()
.and_then(|(_, volt, _, _, _)| {
volt.repository.as_deref()
})
.unwrap_or("")
.to_string();
let local_repo = repo.clone();
stack((
text("Repository: "),
web_link(
move || repo.clone(),
move || local_repo.clone(),
move || {
config
.get()
.color(LapceColor::EDITOR_LINK)
},
internal_command,
),
))
},
text(
plugin_info
.as_ref()
.map(|(_, volt, _, _, _)| volt.author.as_str())
.unwrap_or(""),
)
.style(move |s| {
s.color(config.get().color(LapceColor::EDITOR_DIM))
}),
version_view(local_plugin.clone(), plugin_info.clone()),
))
.style(|s| s.flex_col().line_height(1.6)),
))
.style(|s| s.absolute())
.on_resize(move |rect| {
if header_rect.get_untracked() != rect {
header_rect.set(rect);
}
}),
empty().style(move |s| {
let rect = header_rect.get();
s.size(rect.width(), rect.height()).pointer_events_none()
}),
empty().style(move |s| {
s.margin_vert(6)
.height(1)
.width_full()
.background(config.get().color(LapceColor::LAPCE_BORDER))
}),
{
let readme = create_rw_signal(None);
let info = plugin_info
.as_ref()
.map(|(_, info, _, _, _)| info.to_owned());
create_effect(move |_| {
let config = config.get();
let info = info.clone();
if let Some(info) = info {
let cx = Scope::current();
let send = create_ext_action(cx, move |result| {
if let Ok(md) = result {
readme.set(Some(md));
}
});
std::thread::spawn(move || {
let result =
PluginData::download_readme(&info, &config);
send(result);
});
}
});
{
let id = AtomicU64::new(0);
dyn_stack(
move || {
readme.get().unwrap_or_else(|| {
parse_markdown(
"Loading README",
2.0,
&config.get(),
)
})
},
move |_| {
id.fetch_add(
1,
std::sync::atomic::Ordering::Relaxed,
)
},
move |content| match content {
MarkdownContent::Text(text_layout) => container(
rich_text(move || text_layout.clone())
.style(|s| s.width_full()),
)
.style(|s| s.width_full()),
MarkdownContent::Image { .. } => {
container(empty())
}
MarkdownContent::Separator => {
container(empty().style(move |s| {
s.width_full()
.margin_vert(5.0)
.height(1.0)
.background(
config.get().color(
LapceColor::LAPCE_BORDER,
),
)
}))
}
},
)
.style(|s| s.flex_col().width_full())
}
},
))
.style(move |s| {
let padding = 60.0;
s.flex_col()
.width(
scroll_width
.get()
.min(800.0)
.max(header_rect.get().width() + padding * 2.0),
)
.padding(padding)
})
.into_any()
},
)
.style(|s| s.min_width_full().justify_center()),
)
.on_resize(move |rect| {
if scroll_width.get_untracked() != rect.width() {
scroll_width.set(rect.width());
}
})
.style(|s| s.absolute().size_full())
.debug_name("Plugin Info")
}