/
githubmirror
/
lapce
Обзор
Документация
Войти
/
githubmirror
/
lapce
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
lapce-app/src/file_explorer/view.rs
620 строк
23 KB
Arthur Cosentino
CursorStyle::Pointer consistency (#3819)
31 окт 2025, 18:28
Не верифицирован
31 окт 2025, 18:28
c426503
Код
Авторство
О чём код?
use std::{path::Path, rc::Rc, sync::Arc}; use floem::{ View, event::{Event, EventListener}, kurbo::Rect, peniko::Color, reactive::{ ReadSignal, RwSignal, SignalGet, SignalUpdate, SignalWith, create_rw_signal, }, style::{AlignItems, CursorStyle, Position, Style}, text::Style as FontStyle, views::{ Container, Decorators, container, dyn_stack, label, scroll, stack, svg, virtual_stack, }, }; use lapce_core::selection::Selection; use lapce_rpc::{ file::{FileNodeViewData, FileNodeViewKind, Naming}, source_control::FileDiffKind, }; use lapce_xi_rope::Rope; use super::{data::FileExplorerData, node::FileNodeVirtualList}; use crate::{ app::clickable_icon, command::InternalCommand, config::{LapceConfig, color::LapceColor, icon::LapceIcons}, editor_tab::{EditorTabChild, EditorTabData}, panel::{ data::PanelSection, kind::PanelKind, position::PanelPosition, view::PanelBuilder, }, plugin::PluginData, source_control::SourceControlData, text_input::TextInputBuilder, window_tab::{Focus, WindowTabData}, }; /// Blends `foreground` with `background`. /// /// Uses the alpha channel from `foreground` - if `foreground` is opaque, `foreground` will be /// returned unchanged. /// /// The result is always opaque regardless of the transparency of the inputs. fn blend_colors(background: Color, foreground: Color) -> Color { let background = background.to_rgba8(); let foreground = foreground.to_rgba8(); let a: u16 = foreground.a.into(); let [r, g, b] = [ [background.r, foreground.r], [background.g, foreground.g], [background.b, foreground.b], ] .map(|x| x.map(u16::from)) .map(|[b, f]| (a * f + (255 - a) * b) / 255) .map(|x| x as u8); Color::from_rgba8(r, g, b, 255) } pub fn file_explorer_panel( window_tab_data: Rc<WindowTabData>, position: PanelPosition, ) -> impl View { let config = window_tab_data.common.config; let data = window_tab_data.file_explorer.clone(); let source_control = window_tab_data.source_control.clone(); PanelBuilder::new(config, position) .add_height_style( "Open Editors", 150.0, container(open_editors_view(window_tab_data.clone())) .style(|s| s.size_full()), window_tab_data.panel.section_open(PanelSection::OpenEditor), move |s| s.apply_if(!config.get().ui.open_editors_visible, |s| s.hide()), ) .add( "File Explorer", container(file_explorer_view(data, source_control)) .style(|s| s.size_full()), window_tab_data .panel .section_open(PanelSection::FileExplorer), ) .build() .debug_name("File Explorer Panel") } /// Initialize the file explorer's naming (renaming, creating, etc.) editor with the given path. fn initialize_naming_editor_with_path(data: &FileExplorerData, path: &Path) { let file_name = path.file_name().unwrap_or_default().to_string_lossy(); // Start with the part of the file or directory name before the extension // selected. let selection_end = { let without_leading_dot = file_name.strip_prefix('.').unwrap_or(&file_name); let idx = without_leading_dot .find('.') .unwrap_or(without_leading_dot.len()); idx + file_name.len() - without_leading_dot.len() }; initialize_naming_editor(data, &file_name, Some(selection_end)); } fn initialize_naming_editor( data: &FileExplorerData, text: &str, selection_end: Option<usize>, ) { let text = Rope::from(text); let selection_end = selection_end.unwrap_or(text.len()); let doc = data.naming_editor_data.doc(); doc.reload(text, true); data.naming_editor_data .cursor() .update(|cursor| cursor.set_insert(Selection::region(0, selection_end))); data.naming .update(|naming| naming.set_editor_needs_reset(false)); } fn file_node_text_color( config: ReadSignal<Arc<LapceConfig>>, node: FileNodeViewData, source_control: SourceControlData, ) -> Color { let diff = source_control.file_diffs.with(|file_diffs| { let FileNodeViewKind::Path(path) = &node.kind else { return None; }; if node.is_dir { file_diffs .keys() .find(|p| p.as_path().starts_with(path)) .map(|_| FileDiffKind::Modified) } else { file_diffs.get(path).map(|(diff, _)| diff.kind()) } }); let color = match diff { Some(FileDiffKind::Modified | FileDiffKind::Renamed) => { LapceColor::SOURCE_CONTROL_MODIFIED } Some(FileDiffKind::Added) => LapceColor::SOURCE_CONTROL_ADDED, Some(FileDiffKind::Deleted) => LapceColor::SOURCE_CONTROL_REMOVED, None => LapceColor::PANEL_FOREGROUND, }; config.get().color(color) } fn file_node_text_view( data: FileExplorerData, node: FileNodeViewData, source_control: SourceControlData, ) -> impl View { let config = data.common.config; let ui_line_height = data.common.ui_line_height; match node.kind.clone() { FileNodeViewKind::Path(path) => { if node.is_root { let file = path.clone(); container(( label(move || { file.file_name() .map(|f| f.to_string_lossy().to_string()) .unwrap_or_default() }) .style(move |s| { s.height(ui_line_height.get()) .color(file_node_text_color( config, node.clone(), source_control.clone(), )) .padding_right(5.0) .selectable(false) }), label(move || path.to_string_lossy().to_string()).style( move |s| { s.height(ui_line_height.get()) .color( config .get() .color(LapceColor::PANEL_FOREGROUND_DIM), ) .selectable(false) }, ), )) } else { container( label(move || { path.file_name() .map(|f| f.to_string_lossy().to_string()) .unwrap_or_default() }) .style(move |s| { s.height(ui_line_height.get()) .color(file_node_text_color( config, node.clone(), source_control.clone(), )) .selectable(false) }), ) } } FileNodeViewKind::Renaming { path, err } => { if data.naming.with_untracked(Naming::editor_needs_reset) { initialize_naming_editor_with_path(&data, &path); } file_node_input_view(data, err.clone()) } FileNodeViewKind::Naming { err } => { if data.naming.with_untracked(Naming::editor_needs_reset) { initialize_naming_editor(&data, "", None); } file_node_input_view(data, err.clone()) } FileNodeViewKind::Duplicating { source, err } => { if data.naming.with_untracked(Naming::editor_needs_reset) { initialize_naming_editor_with_path(&data, &source); } file_node_input_view(data, err.clone()) } } } /// Input used for naming a file/directory fn file_node_input_view(data: FileExplorerData, err: Option<String>) -> Container { let ui_line_height = data.common.ui_line_height; let naming_editor_data = data.naming_editor_data.clone(); let text_input_file_explorer_data = data.clone(); let focus = data.common.focus; let config = data.common.config; let is_focused = move || { focus.with_untracked(|focus| focus == &Focus::Panel(PanelKind::FileExplorer)) }; let text_input_view = TextInputBuilder::new() .is_focused(is_focused) .key_focus(text_input_file_explorer_data) .build_editor(naming_editor_data.clone()) .on_event_stop(EventListener::FocusLost, move |_| { data.finish_naming(); data.naming.set(Naming::None); }) .style(move |s| { s.width_full() .height(ui_line_height.get()) .padding(0.0) .margin(0.0) .border_radius(6.0) .border(1.0) .border_color(config.get().color(LapceColor::LAPCE_BORDER)) }); let text_input_id = text_input_view.id(); text_input_id.request_focus(); if let Some(err) = err { container( stack(( text_input_view, label(move || err.clone()).style(move |s| { let config = config.get(); let editor_background_color = config.color(LapceColor::PANEL_CURRENT_BACKGROUND); let error_background_color = config.color(LapceColor::ERROR_LENS_ERROR_BACKGROUND); let background_color = blend_colors( editor_background_color, error_background_color, ); s.position(Position::Absolute) .inset_top(ui_line_height.get()) .width_full() .color(config.color(LapceColor::ERROR_LENS_ERROR_FOREGROUND)) .background(background_color) .z_index(100) }), )) .style(|s| s.flex_grow(1.0)), ) } else { container(text_input_view) } .style(move |s| s.width_full()) } fn file_explorer_view( data: FileExplorerData, source_control: SourceControlData, ) -> impl View { let root = data.root; let ui_line_height = data.common.ui_line_height; let config = data.common.config; let naming = data.naming; let scroll_to_line = data.scroll_to_line; let select = data.select; let secondary_click_data = data.clone(); let scroll_rect = create_rw_signal(Rect::ZERO); scroll( virtual_stack( move || FileNodeVirtualList::new(root.get(), data.naming.get()), move |node| (node.kind.clone(), node.is_dir, node.open, node.level), move |node| { let level = node.level; let data = data.clone(); let click_data = data.clone(); let double_click_data = data.clone(); let secondary_click_data = data.clone(); let aux_click_data = data.clone(); let kind = node.kind.clone(); let open = node.open; let is_dir = node.is_dir; let view = stack(( svg(move || { let config = config.get(); let svg_str = match open { true => LapceIcons::ITEM_OPENED, false => LapceIcons::ITEM_CLOSED, }; config.ui_svg(svg_str) }) .style(move |s| { let config = config.get(); let size = config.ui.icon_size() as f32; let color = if is_dir { config.color(LapceColor::LAPCE_ICON_ACTIVE) } else { Color::TRANSPARENT }; s.size(size, size) .flex_shrink(0.0) .margin_left(10.0) .color(color) }), { let kind = kind.clone(); let kind_for_style = kind.clone(); // TODO: use the current naming input as the path for the file svg svg(move || { let config = config.get(); if is_dir { let svg_str = match open { true => LapceIcons::DIRECTORY_OPENED, false => LapceIcons::DIRECTORY_CLOSED, }; config.ui_svg(svg_str) } else if let Some(path) = kind.path() { config.file_svg(path).0 } else { config.ui_svg(LapceIcons::FILE) } }) .style(move |s| { let config = config.get(); let size = config.ui.icon_size() as f32; s.size(size, size) .flex_shrink(0.0) .margin_horiz(6.0) .apply_if(is_dir, |s| { s.color( config.color(LapceColor::LAPCE_ICON_ACTIVE), ) }) .apply_if(!is_dir, |s| { s.apply_opt( kind_for_style .path() .and_then(|p| config.file_svg(p).1), Style::color, ) }) }) }, file_node_text_view(data, node, source_control.clone()), )) .style({ let kind = kind.clone(); move |s| { s.padding_right(15.0) .min_width_full() .padding_left((level * 10) as f32) .align_items(AlignItems::Center) .hover(|s| { s.background( config .get() .color(LapceColor::PANEL_HOVERED_BACKGROUND), ) .cursor(CursorStyle::Pointer) }) .apply_if( select.get().map(|x| x == kind).unwrap_or_default(), |x| { x.background( config.get().color( LapceColor::PANEL_CURRENT_BACKGROUND, ), ) }, ) } }) .debug_name("file item"); // Only handle click events if we are not naming the file node if let FileNodeViewKind::Path(path) = &kind { let click_path = path.clone(); let double_click_path = path.clone(); let secondary_click_path = path.clone(); let aux_click_path = path.clone(); view.on_click_stop({ let kind = kind.clone(); move |_| { click_data.click(&click_path, config); select.update(|x| *x = Some(kind.clone())); } }) .on_double_click({ move |_| { double_click_data .double_click(&double_click_path, config) } }) .on_secondary_click_stop(move |_| { secondary_click_data.secondary_click(&secondary_click_path); }) .on_event_stop( EventListener::PointerDown, move |event| { if let Event::PointerDown(pointer_event) = event { if pointer_event.button.is_auxiliary() { aux_click_data.middle_click(&aux_click_path); } } }, ) } else { view } }, ) .item_size_fixed(move || ui_line_height.get()) .style(|s| s.absolute().flex_col().min_width_full()), ) .style(|s| s.absolute().size_full().line_height(1.8)) .on_secondary_click_stop(move |_| { if let Naming::None = naming.get_untracked() { if let Some(path) = &secondary_click_data.common.workspace.path { secondary_click_data.secondary_click(path); } } }) .on_resize(move |rect| { scroll_rect.set(rect); }) .scroll_to(move || { if let Some(line) = scroll_to_line.get() { let line_height = ui_line_height.get_untracked(); Some( ( 0.0, line * line_height - scroll_rect.get_untracked().height() / 2.0, ) .into(), ) } else { None } }) } fn open_editors_view(window_tab_data: Rc<WindowTabData>) -> impl View { let diff_editors = window_tab_data.main_split.diff_editors; let editors = window_tab_data.main_split.editors; let editor_tabs = window_tab_data.main_split.editor_tabs; let config = window_tab_data.common.config; let internal_command = window_tab_data.common.internal_command; let active_editor_tab = window_tab_data.main_split.active_editor_tab; let plugin = window_tab_data.plugin.clone(); let child_view = move |plugin: PluginData, editor_tab: RwSignal<EditorTabData>, child_index: RwSignal<usize>, child: EditorTabChild| { let editor_tab_id = editor_tab.with_untracked(|editor_tab| editor_tab.editor_tab_id); let child_for_close = child.clone(); let info = child.view_info(editors, diff_editors, plugin, config); let hovered = create_rw_signal(false); stack(( clickable_icon( move || { if hovered.get() || info.with(|info| info.is_pristine) { LapceIcons::CLOSE } else { LapceIcons::UNSAVED } }, move || { let editor_tab_id = editor_tab.with_untracked(|t| t.editor_tab_id); internal_command.send(InternalCommand::EditorTabChildClose { editor_tab_id, child: child_for_close.clone(), }); }, || false, || false, || "Close", config, ) .on_event_stop(EventListener::PointerEnter, move |_| { hovered.set(true); }) .on_event_stop(EventListener::PointerLeave, move |_| { hovered.set(false); }) .on_event_stop(EventListener::PointerDown, |_| {}) .style(|s| s.margin_left(10.0)), container(svg(move || info.with(|info| info.icon.clone())).style( move |s| { let size = config.get().ui.icon_size() as f32; s.size(size, size) .apply_opt(info.with(|info| info.color), |s, c| s.color(c)) }, )) .style(|s| s.padding_horiz(6.0)), label(move || info.with(|info| info.name.clone())).style(move |s| { s.apply_if( !info .with(|info| info.confirmed) .map(|confirmed| confirmed.get()) .unwrap_or(true), |s| s.font_style(FontStyle::Italic), ) }), )) .style(move |s| { let config = config.get(); s.items_center() .width_pct(100.0) .cursor(CursorStyle::Pointer) .apply_if( active_editor_tab.get() == Some(editor_tab_id) && editor_tab.with(|editor_tab| editor_tab.active) == child_index.get(), |s| { s.background( config.color(LapceColor::PANEL_CURRENT_BACKGROUND), ) }, ) .hover(|s| { s.background(config.color(LapceColor::PANEL_HOVERED_BACKGROUND)) }) }) .on_event_cont(EventListener::PointerDown, move |_| { editor_tab.update(|editor_tab| { editor_tab.active = child_index.get_untracked(); }); active_editor_tab.set(Some(editor_tab_id)); }) }; scroll( dyn_stack( move || editor_tabs.get().into_iter().enumerate(), move |(index, (editor_tab_id, _))| (*index, *editor_tab_id), move |(index, (_, editor_tab))| { let plugin = plugin.clone(); stack(( label(move || format!("Group {}", index + 1)) .style(|s| s.margin_left(10.0)), dyn_stack( move || editor_tab.get().children, move |(_, _, child)| child.id(), move |(child_index, _, child)| { child_view( plugin.clone(), editor_tab, child_index, child, ) }, ) .style(|s| s.flex_col().width_pct(100.0)), )) .style(|s| s.flex_col()) }, ) .style(|s| s.flex_col().width_pct(100.0)), ) .style(|s| s.absolute().size_full().line_height(1.8)) .debug_name("Open Editors") }