mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-30 07:14:46 +00:00
refactoring to separate UI folder for future UI framework
This commit is contained in:
parent
9dda23cf8b
commit
d289dc9f73
34 changed files with 900 additions and 2962 deletions
6
editor/src/editor/colors.rs
Normal file
6
editor/src/editor/colors.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
use crate::ui::colors::ColorTup;
|
||||||
|
|
||||||
|
pub const TXT_COLOR: ColorTup = (1.0, 1.0, 1.0, 1.0);
|
||||||
|
pub const CODE_COLOR: ColorTup = (0.21, 0.55, 0.83, 1.0);
|
||||||
|
pub const BG_COLOR: ColorTup = (0.11, 0.11, 0.13, 1.0);
|
||||||
|
|
|
@ -21,13 +21,6 @@ pub enum EdError {
|
||||||
))]
|
))]
|
||||||
ClipboardInitFailed { err_msg: String },
|
ClipboardInitFailed { err_msg: String },
|
||||||
|
|
||||||
#[snafu(display(
|
|
||||||
"FileOpenFailed: failed to open file with path {} with the following error: {}.",
|
|
||||||
path_str,
|
|
||||||
err_msg
|
|
||||||
))]
|
|
||||||
FileOpenFailed { path_str: String, err_msg: String },
|
|
||||||
|
|
||||||
#[snafu(display("InvalidSelection: {}.", err_msg))]
|
#[snafu(display("InvalidSelection: {}.", err_msg))]
|
||||||
InvalidSelection {
|
InvalidSelection {
|
||||||
err_msg: String,
|
err_msg: String,
|
||||||
|
@ -49,9 +42,6 @@ pub enum EdError {
|
||||||
len: usize,
|
len: usize,
|
||||||
backtrace: Backtrace,
|
backtrace: Backtrace,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[snafu(display("TextBufReadFailed: the file {} could be opened but we encountered the following error while trying to read it: {}.", path_str, err_msg))]
|
|
||||||
TextBufReadFailed { path_str: String, err_msg: String },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type EdResult<T, E = EdError> = std::result::Result<T, E>;
|
pub type EdResult<T, E = EdError> = std::result::Result<T, E>;
|
449
editor/src/editor/main.rs
Normal file
449
editor/src/editor/main.rs
Normal file
|
@ -0,0 +1,449 @@
|
||||||
|
|
||||||
|
use crate::error::{print_err, EdResult};
|
||||||
|
use crate::graphics::colors::{CODE_COLOR, TXT_COLOR};
|
||||||
|
use crate::graphics::lowlevel::buffer::create_rect_buffers;
|
||||||
|
use crate::graphics::lowlevel::ortho::update_ortho_buffer;
|
||||||
|
use crate::graphics::lowlevel::pipelines;
|
||||||
|
use crate::graphics::primitives::text::{
|
||||||
|
build_glyph_brush, example_code_glyph_rect, queue_code_text_draw, queue_text_draw, Text,
|
||||||
|
};
|
||||||
|
use crate::graphics::style::CODE_FONT_SIZE;
|
||||||
|
use crate::graphics::style::CODE_TXT_XY;
|
||||||
|
use crate::mvc::app_model::AppModel;
|
||||||
|
use crate::mvc::ed_model::EdModel;
|
||||||
|
use crate::mvc::{app_update, ed_model, ed_view};
|
||||||
|
//use crate::resources::strings::NOTHING_OPENED;
|
||||||
|
use crate::vec_result::get_res;
|
||||||
|
use bumpalo::Bump;
|
||||||
|
use cgmath::Vector2;
|
||||||
|
use ed_model::Position;
|
||||||
|
use lang::{pool::Pool, scope::Scope};
|
||||||
|
use pipelines::RectResources;
|
||||||
|
use roc_collections::all::MutMap;
|
||||||
|
use roc_module::symbol::{IdentIds, ModuleIds};
|
||||||
|
use roc_region::all::Region;
|
||||||
|
use roc_types::subs::VarStore;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::io;
|
||||||
|
use std::path::Path;
|
||||||
|
use wgpu::{CommandEncoder, RenderPass, TextureView};
|
||||||
|
use wgpu_glyph::GlyphBrush;
|
||||||
|
use winit::dpi::PhysicalSize;
|
||||||
|
use winit::event;
|
||||||
|
use winit::event::{Event, ModifiersState};
|
||||||
|
use winit::event_loop::ControlFlow;
|
||||||
|
|
||||||
|
// Inspired by:
|
||||||
|
// https://github.com/sotrh/learn-wgpu by Benjamin Hansen, licensed under the MIT license
|
||||||
|
// https://github.com/cloudhead/rgx by Alexis Sellier, licensed under the MIT license
|
||||||
|
|
||||||
|
// See this link to learn wgpu: https://sotrh.github.io/learn-wgpu/
|
||||||
|
|
||||||
|
/// The editor is actually launched from the CLI if you pass it zero arguments,
|
||||||
|
/// or if you provide it 1 or more files or directories to open on launch.
|
||||||
|
pub fn launch(filepaths: &[&Path]) -> io::Result<()> {
|
||||||
|
//TODO support using multiple filepaths
|
||||||
|
let first_path_opt = if !filepaths.is_empty() {
|
||||||
|
match get_res(0, filepaths) {
|
||||||
|
Ok(path_ref_ref) => Some(*path_ref_ref),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
run_event_loop(first_path_opt).expect("Error running event loop");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_event_loop(file_path_opt: Option<&Path>) -> Result<(), Box<dyn Error>> {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
// Open window and create a surface
|
||||||
|
let event_loop = winit::event_loop::EventLoop::new();
|
||||||
|
|
||||||
|
let window = winit::window::WindowBuilder::new()
|
||||||
|
.with_inner_size(PhysicalSize::new(1200.0, 1000.0))
|
||||||
|
.build(&event_loop)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let instance = wgpu::Instance::new(wgpu::BackendBit::all());
|
||||||
|
|
||||||
|
let surface = unsafe { instance.create_surface(&window) };
|
||||||
|
|
||||||
|
// Initialize GPU
|
||||||
|
let (gpu_device, cmd_queue) = futures::executor::block_on(async {
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Request adapter");
|
||||||
|
|
||||||
|
adapter
|
||||||
|
.request_device(
|
||||||
|
&wgpu::DeviceDescriptor {
|
||||||
|
features: wgpu::Features::empty(),
|
||||||
|
limits: wgpu::Limits::default(),
|
||||||
|
shader_validation: false,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Request device")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create staging belt and a local pool
|
||||||
|
let mut staging_belt = wgpu::util::StagingBelt::new(1024);
|
||||||
|
let mut local_pool = futures::executor::LocalPool::new();
|
||||||
|
let local_spawner = local_pool.spawner();
|
||||||
|
|
||||||
|
// Prepare swap chain
|
||||||
|
let render_format = wgpu::TextureFormat::Bgra8Unorm;
|
||||||
|
let mut size = window.inner_size();
|
||||||
|
|
||||||
|
let swap_chain_descr = wgpu::SwapChainDescriptor {
|
||||||
|
usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
|
||||||
|
format: render_format,
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
//Immediate may cause tearing, change present_mode if this becomes a problem
|
||||||
|
present_mode: wgpu::PresentMode::Immediate,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut swap_chain = gpu_device.create_swap_chain(&surface, &swap_chain_descr);
|
||||||
|
|
||||||
|
let rect_resources = pipelines::make_rect_pipeline(&gpu_device, &swap_chain_descr);
|
||||||
|
|
||||||
|
let mut glyph_brush = build_glyph_brush(&gpu_device, render_format)?;
|
||||||
|
|
||||||
|
let is_animating = true;
|
||||||
|
let ed_model_opt = if let Some(file_path) = file_path_opt {
|
||||||
|
let ed_model_res = ed_model::init_model(file_path);
|
||||||
|
|
||||||
|
match ed_model_res {
|
||||||
|
Ok(mut ed_model) => {
|
||||||
|
ed_model.glyph_dim_rect_opt = Some(example_code_glyph_rect(&mut glyph_brush));
|
||||||
|
|
||||||
|
Some(ed_model)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
print_err(&e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut app_model = AppModel::init(ed_model_opt);
|
||||||
|
|
||||||
|
let mut keyboard_modifiers = ModifiersState::empty();
|
||||||
|
|
||||||
|
// This arena is never cleared and should only be used for allocations that occur rarely
|
||||||
|
let arena = Bump::new();
|
||||||
|
|
||||||
|
let mut rects_arena = Bump::new();
|
||||||
|
|
||||||
|
// Render loop
|
||||||
|
window.request_redraw();
|
||||||
|
|
||||||
|
event_loop.run(move |event, _, control_flow| {
|
||||||
|
// TODO dynamically switch this on/off depending on whether any
|
||||||
|
// animations are running. Should conserve CPU usage and battery life!
|
||||||
|
if is_animating {
|
||||||
|
*control_flow = ControlFlow::Poll;
|
||||||
|
} else {
|
||||||
|
*control_flow = ControlFlow::Wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
match event {
|
||||||
|
//Close
|
||||||
|
Event::WindowEvent {
|
||||||
|
event: event::WindowEvent::CloseRequested,
|
||||||
|
..
|
||||||
|
} => *control_flow = ControlFlow::Exit,
|
||||||
|
//Resize
|
||||||
|
Event::WindowEvent {
|
||||||
|
event: event::WindowEvent::Resized(new_size),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
size = new_size;
|
||||||
|
|
||||||
|
swap_chain = gpu_device.create_swap_chain(
|
||||||
|
&surface,
|
||||||
|
&wgpu::SwapChainDescriptor {
|
||||||
|
usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
|
||||||
|
format: render_format,
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
//Immediate may cause tearing, change present_mode if this becomes a problem
|
||||||
|
present_mode: wgpu::PresentMode::Immediate,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
update_ortho_buffer(
|
||||||
|
size.width,
|
||||||
|
size.height,
|
||||||
|
&gpu_device,
|
||||||
|
&rect_resources.ortho.buffer,
|
||||||
|
&cmd_queue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
//Received Character
|
||||||
|
Event::WindowEvent {
|
||||||
|
event: event::WindowEvent::ReceivedCharacter(ch),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if let Err(e) = app_update::handle_new_char(&ch, &mut app_model) {
|
||||||
|
print_err(&e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Keyboard Input
|
||||||
|
Event::WindowEvent {
|
||||||
|
event: event::WindowEvent::KeyboardInput { input, .. },
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if let Some(virtual_keycode) = input.virtual_keycode {
|
||||||
|
if let Some(ref mut ed_model) = app_model.ed_model_opt {
|
||||||
|
if ed_model.has_focus {
|
||||||
|
let keydown_res = keyboard_input::handle_keydown(
|
||||||
|
input.state,
|
||||||
|
virtual_keycode,
|
||||||
|
keyboard_modifiers,
|
||||||
|
&mut app_model,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = keydown_res {
|
||||||
|
print_err(&e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Modifiers Changed
|
||||||
|
Event::WindowEvent {
|
||||||
|
event: event::WindowEvent::ModifiersChanged(modifiers),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
keyboard_modifiers = modifiers;
|
||||||
|
}
|
||||||
|
Event::MainEventsCleared => window.request_redraw(),
|
||||||
|
Event::RedrawRequested { .. } => {
|
||||||
|
// Get a command encoder for the current frame
|
||||||
|
let mut encoder =
|
||||||
|
gpu_device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("Redraw"),
|
||||||
|
});
|
||||||
|
|
||||||
|
let frame = swap_chain
|
||||||
|
.get_current_frame()
|
||||||
|
.expect("Failed to acquire next SwapChainFrame")
|
||||||
|
.output;
|
||||||
|
|
||||||
|
if let Some(ed_model) = &app_model.ed_model_opt {
|
||||||
|
//TODO don't pass invisible lines
|
||||||
|
queue_editor_text(
|
||||||
|
&size,
|
||||||
|
&ed_model.text_buf.all_lines(&arena),
|
||||||
|
ed_model.caret_pos,
|
||||||
|
CODE_TXT_XY.into(),
|
||||||
|
&mut glyph_brush,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// queue_no_file_text(&size, NOTHING_OPENED, CODE_TXT_XY.into(), &mut glyph_brush);
|
||||||
|
|
||||||
|
let mut pool = Pool::with_capacity(12);
|
||||||
|
let mut var_store = VarStore::default();
|
||||||
|
let dep_idents = MutMap::default();
|
||||||
|
let mut module_ids = ModuleIds::default();
|
||||||
|
let exposed_ident_ids = IdentIds::default();
|
||||||
|
|
||||||
|
let home = module_ids.get_or_insert(&"Home".into());
|
||||||
|
|
||||||
|
let mut env = crate::lang::expr::Env::new(
|
||||||
|
home,
|
||||||
|
&arena,
|
||||||
|
&mut pool,
|
||||||
|
&mut var_store,
|
||||||
|
dep_idents,
|
||||||
|
&module_ids,
|
||||||
|
exposed_ident_ids,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut scope = Scope::new(home, env.pool, env.var_store);
|
||||||
|
|
||||||
|
let region = Region::new(0, 0, 0, 0);
|
||||||
|
|
||||||
|
let (expr2, _) = crate::lang::expr::str_to_expr2(
|
||||||
|
&arena, "True", &mut env, &mut scope, region,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
render::render_expr2(
|
||||||
|
&mut env,
|
||||||
|
&size,
|
||||||
|
&expr2,
|
||||||
|
CODE_TXT_XY.into(),
|
||||||
|
&mut glyph_brush,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
rects_arena.reset();
|
||||||
|
|
||||||
|
match draw_all_rects(
|
||||||
|
&app_model.ed_model_opt,
|
||||||
|
&rects_arena,
|
||||||
|
&mut encoder,
|
||||||
|
&frame.view,
|
||||||
|
&gpu_device,
|
||||||
|
&rect_resources,
|
||||||
|
) {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(e) => {
|
||||||
|
print_err(&e);
|
||||||
|
begin_render_pass(&mut encoder, &frame.view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw all text
|
||||||
|
glyph_brush
|
||||||
|
.draw_queued(
|
||||||
|
&gpu_device,
|
||||||
|
&mut staging_belt,
|
||||||
|
&mut encoder,
|
||||||
|
&frame.view,
|
||||||
|
size.width,
|
||||||
|
size.height,
|
||||||
|
)
|
||||||
|
.expect("Draw queued");
|
||||||
|
|
||||||
|
staging_belt.finish();
|
||||||
|
cmd_queue.submit(Some(encoder.finish()));
|
||||||
|
|
||||||
|
// Recall unused staging buffers
|
||||||
|
use futures::task::SpawnExt;
|
||||||
|
|
||||||
|
local_spawner
|
||||||
|
.spawn(staging_belt.recall())
|
||||||
|
.expect("Recall staging belt");
|
||||||
|
|
||||||
|
local_pool.run_until_stalled();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
*control_flow = winit::event_loop::ControlFlow::Wait;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_all_rects(
|
||||||
|
ed_model_opt: &Option<EdModel>,
|
||||||
|
arena: &Bump,
|
||||||
|
encoder: &mut CommandEncoder,
|
||||||
|
texture_view: &TextureView,
|
||||||
|
gpu_device: &wgpu::Device,
|
||||||
|
rect_resources: &RectResources,
|
||||||
|
) -> EdResult<()> {
|
||||||
|
if let Some(ed_model) = ed_model_opt {
|
||||||
|
let all_rects = ed_view::create_ed_rects(ed_model, arena)?;
|
||||||
|
|
||||||
|
let rect_buffers = create_rect_buffers(gpu_device, encoder, &all_rects);
|
||||||
|
|
||||||
|
let mut render_pass = begin_render_pass(encoder, texture_view);
|
||||||
|
|
||||||
|
render_pass.set_pipeline(&rect_resources.pipeline);
|
||||||
|
render_pass.set_bind_group(0, &rect_resources.ortho.bind_group, &[]);
|
||||||
|
render_pass.set_vertex_buffer(0, rect_buffers.vertex_buffer.slice(..));
|
||||||
|
render_pass.set_index_buffer(rect_buffers.index_buffer.slice(..));
|
||||||
|
render_pass.draw_indexed(0..rect_buffers.num_rects, 0, 0..1);
|
||||||
|
} else {
|
||||||
|
// need to begin render pass to clear screen
|
||||||
|
begin_render_pass(encoder, texture_view);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_render_pass<'a>(
|
||||||
|
encoder: &'a mut CommandEncoder,
|
||||||
|
texture_view: &'a TextureView,
|
||||||
|
) -> RenderPass<'a> {
|
||||||
|
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor {
|
||||||
|
attachment: texture_view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||||
|
r: 0.1,
|
||||||
|
g: 0.2,
|
||||||
|
b: 0.3,
|
||||||
|
a: 1.0,
|
||||||
|
}),
|
||||||
|
store: true,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns bounding boxes for every glyph
|
||||||
|
fn queue_editor_text(
|
||||||
|
size: &PhysicalSize<u32>,
|
||||||
|
editor_lines: &str,
|
||||||
|
caret_pos: Position,
|
||||||
|
code_coords: Vector2<f32>,
|
||||||
|
glyph_brush: &mut GlyphBrush<()>,
|
||||||
|
) {
|
||||||
|
let area_bounds = (size.width as f32, size.height as f32).into();
|
||||||
|
|
||||||
|
let code_text = Text {
|
||||||
|
position: code_coords,
|
||||||
|
area_bounds,
|
||||||
|
color: CODE_COLOR.into(),
|
||||||
|
text: editor_lines,
|
||||||
|
size: CODE_FONT_SIZE,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let s = format!("Ln {}, Col {}", caret_pos.line, caret_pos.column);
|
||||||
|
let text = s.as_str();
|
||||||
|
|
||||||
|
let caret_pos_label = Text {
|
||||||
|
position: ((size.width as f32) - 150.0, (size.height as f32) - 40.0).into(),
|
||||||
|
area_bounds,
|
||||||
|
color: TXT_COLOR.into(),
|
||||||
|
text,
|
||||||
|
size: 25.0,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
queue_text_draw(&caret_pos_label, glyph_brush);
|
||||||
|
|
||||||
|
queue_code_text_draw(&code_text, glyph_brush);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _queue_no_file_text(
|
||||||
|
size: &PhysicalSize<u32>,
|
||||||
|
text: &str,
|
||||||
|
text_coords: Vector2<f32>,
|
||||||
|
glyph_brush: &mut GlyphBrush<()>,
|
||||||
|
) {
|
||||||
|
let area_bounds = (size.width as f32, size.height as f32).into();
|
||||||
|
|
||||||
|
let code_text = Text {
|
||||||
|
position: text_coords,
|
||||||
|
area_bounds,
|
||||||
|
color: CODE_COLOR.into(),
|
||||||
|
text,
|
||||||
|
size: CODE_FONT_SIZE,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
queue_text_draw(&code_text, glyph_brush);
|
||||||
|
}
|
6
editor/src/editor/mod.rs
Normal file
6
editor/src/editor/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
pub mod main;
|
||||||
|
mod ed_error;
|
||||||
|
mod keyboard_input;
|
||||||
|
mod render;
|
||||||
|
mod colors;
|
|
@ -1,10 +1,15 @@
|
||||||
use crate::error::EdResult;
|
|
||||||
use crate::error::OutOfBounds;
|
use super::ed_error::{EdResult, OutOfBounds};
|
||||||
use snafu::OptionExt;
|
use snafu::OptionExt;
|
||||||
use std::slice::SliceIndex;
|
use std::slice::SliceIndex;
|
||||||
|
|
||||||
// replace vec methods that return Option with ones that return Result and proper Error
|
pub fn is_newline(char_ref: &char) -> bool {
|
||||||
|
let newline_codes = vec!['\u{d}', '\n'];
|
||||||
|
|
||||||
|
newline_codes.contains(char_ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace vec methods that return Option with ones that return Result and proper Error
|
||||||
pub fn get_res<T>(index: usize, slice: &[T]) -> EdResult<&<usize as SliceIndex<[T]>>::Output> {
|
pub fn get_res<T>(index: usize, slice: &[T]) -> EdResult<&<usize as SliceIndex<[T]>>::Output> {
|
||||||
let elt_ref = slice.get(index).context(OutOfBounds {
|
let elt_ref = slice.get(index).context(OutOfBounds {
|
||||||
index,
|
index,
|
|
@ -2,471 +2,19 @@
|
||||||
// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check.
|
// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check.
|
||||||
#![allow(clippy::large_enum_variant)]
|
#![allow(clippy::large_enum_variant)]
|
||||||
|
|
||||||
// Inspired by:
|
|
||||||
// https://github.com/sotrh/learn-wgpu by Benjamin Hansen, licensed under the MIT license
|
|
||||||
// https://github.com/cloudhead/rgx by Alexis Sellier, licensed under the MIT license
|
|
||||||
|
|
||||||
// See this link to learn wgpu: https://sotrh.github.io/learn-wgpu/
|
|
||||||
|
|
||||||
extern crate pest;
|
extern crate pest;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate pest_derive;
|
extern crate pest_derive;
|
||||||
|
|
||||||
use crate::error::{print_err, EdResult};
|
mod graphics;
|
||||||
use crate::graphics::colors::{CODE_COLOR, TXT_COLOR};
|
mod lang;
|
||||||
use crate::graphics::lowlevel::buffer::create_rect_buffers;
|
mod editor;
|
||||||
use crate::graphics::lowlevel::ortho::update_ortho_buffer;
|
mod ui;
|
||||||
use crate::graphics::lowlevel::pipelines;
|
|
||||||
use crate::graphics::primitives::text::{
|
|
||||||
build_glyph_brush, example_code_glyph_rect, queue_code_text_draw, queue_text_draw, Text,
|
|
||||||
};
|
|
||||||
use crate::graphics::style::CODE_FONT_SIZE;
|
|
||||||
use crate::graphics::style::CODE_TXT_XY;
|
|
||||||
use crate::mvc::app_model::AppModel;
|
|
||||||
use crate::mvc::ed_model::EdModel;
|
|
||||||
use crate::mvc::{app_update, ed_model, ed_view};
|
|
||||||
//use crate::resources::strings::NOTHING_OPENED;
|
|
||||||
use crate::vec_result::get_res;
|
|
||||||
use bumpalo::Bump;
|
|
||||||
use cgmath::Vector2;
|
|
||||||
use ed_model::Position;
|
|
||||||
use lang::{pool::Pool, scope::Scope};
|
|
||||||
use pipelines::RectResources;
|
|
||||||
use roc_collections::all::MutMap;
|
|
||||||
use roc_module::symbol::{IdentIds, ModuleIds};
|
|
||||||
use roc_region::all::Region;
|
|
||||||
use roc_types::subs::VarStore;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use wgpu::{CommandEncoder, RenderPass, TextureView};
|
|
||||||
use wgpu_glyph::GlyphBrush;
|
|
||||||
use winit::dpi::PhysicalSize;
|
|
||||||
use winit::event;
|
|
||||||
use winit::event::{Event, ModifiersState};
|
|
||||||
use winit::event_loop::ControlFlow;
|
|
||||||
|
|
||||||
pub mod error;
|
|
||||||
pub mod graphics;
|
|
||||||
mod keyboard_input;
|
|
||||||
pub mod lang;
|
|
||||||
mod mvc;
|
|
||||||
//pub mod mvc; // for benchmarking
|
|
||||||
mod ui;
|
|
||||||
mod resources;
|
|
||||||
mod selection;
|
|
||||||
mod text_buffer;
|
|
||||||
//pub mod text_buffer; // for benchmarking
|
|
||||||
mod render;
|
|
||||||
mod util;
|
|
||||||
mod vec_result;
|
|
||||||
|
|
||||||
/// The editor is actually launched from the CLI if you pass it zero arguments,
|
|
||||||
/// or if you provide it 1 or more files or directories to open on launch.
|
|
||||||
pub fn launch(filepaths: &[&Path]) -> io::Result<()> {
|
pub fn launch(filepaths: &[&Path]) -> io::Result<()> {
|
||||||
//TODO support using multiple filepaths
|
editor::main::launch(filepaths)
|
||||||
let first_path_opt = if !filepaths.is_empty() {
|
|
||||||
match get_res(0, filepaths) {
|
|
||||||
Ok(path_ref_ref) => Some(*path_ref_ref),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
run_event_loop(first_path_opt).expect("Error running event loop");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_event_loop(file_path_opt: Option<&Path>) -> Result<(), Box<dyn Error>> {
|
|
||||||
env_logger::init();
|
|
||||||
|
|
||||||
// Open window and create a surface
|
|
||||||
let event_loop = winit::event_loop::EventLoop::new();
|
|
||||||
|
|
||||||
let window = winit::window::WindowBuilder::new()
|
|
||||||
.with_inner_size(PhysicalSize::new(1200.0, 1000.0))
|
|
||||||
.build(&event_loop)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let instance = wgpu::Instance::new(wgpu::BackendBit::all());
|
|
||||||
|
|
||||||
let surface = unsafe { instance.create_surface(&window) };
|
|
||||||
|
|
||||||
// Initialize GPU
|
|
||||||
let (gpu_device, cmd_queue) = futures::executor::block_on(async {
|
|
||||||
let adapter = instance
|
|
||||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
|
||||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
|
||||||
compatible_surface: Some(&surface),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("Request adapter");
|
|
||||||
|
|
||||||
adapter
|
|
||||||
.request_device(
|
|
||||||
&wgpu::DeviceDescriptor {
|
|
||||||
features: wgpu::Features::empty(),
|
|
||||||
limits: wgpu::Limits::default(),
|
|
||||||
shader_validation: false,
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Request device")
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create staging belt and a local pool
|
|
||||||
let mut staging_belt = wgpu::util::StagingBelt::new(1024);
|
|
||||||
let mut local_pool = futures::executor::LocalPool::new();
|
|
||||||
let local_spawner = local_pool.spawner();
|
|
||||||
|
|
||||||
// Prepare swap chain
|
|
||||||
let render_format = wgpu::TextureFormat::Bgra8Unorm;
|
|
||||||
let mut size = window.inner_size();
|
|
||||||
|
|
||||||
let swap_chain_descr = wgpu::SwapChainDescriptor {
|
|
||||||
usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
|
|
||||||
format: render_format,
|
|
||||||
width: size.width,
|
|
||||||
height: size.height,
|
|
||||||
//Immediate may cause tearing, change present_mode if this becomes a problem
|
|
||||||
present_mode: wgpu::PresentMode::Immediate,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut swap_chain = gpu_device.create_swap_chain(&surface, &swap_chain_descr);
|
|
||||||
|
|
||||||
let rect_resources = pipelines::make_rect_pipeline(&gpu_device, &swap_chain_descr);
|
|
||||||
|
|
||||||
let mut glyph_brush = build_glyph_brush(&gpu_device, render_format)?;
|
|
||||||
|
|
||||||
let is_animating = true;
|
|
||||||
let ed_model_opt = if let Some(file_path) = file_path_opt {
|
|
||||||
let ed_model_res = ed_model::init_model(file_path);
|
|
||||||
|
|
||||||
match ed_model_res {
|
|
||||||
Ok(mut ed_model) => {
|
|
||||||
ed_model.glyph_dim_rect_opt = Some(example_code_glyph_rect(&mut glyph_brush));
|
|
||||||
|
|
||||||
Some(ed_model)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
print_err(&e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut app_model = AppModel::init(ed_model_opt);
|
|
||||||
|
|
||||||
let mut keyboard_modifiers = ModifiersState::empty();
|
|
||||||
|
|
||||||
// This arena is never cleared and should only be used for allocations that occur rarely
|
|
||||||
let arena = Bump::new();
|
|
||||||
|
|
||||||
let mut rects_arena = Bump::new();
|
|
||||||
|
|
||||||
// Render loop
|
|
||||||
window.request_redraw();
|
|
||||||
|
|
||||||
event_loop.run(move |event, _, control_flow| {
|
|
||||||
// TODO dynamically switch this on/off depending on whether any
|
|
||||||
// animations are running. Should conserve CPU usage and battery life!
|
|
||||||
if is_animating {
|
|
||||||
*control_flow = ControlFlow::Poll;
|
|
||||||
} else {
|
|
||||||
*control_flow = ControlFlow::Wait;
|
|
||||||
}
|
|
||||||
|
|
||||||
match event {
|
|
||||||
//Close
|
|
||||||
Event::WindowEvent {
|
|
||||||
event: event::WindowEvent::CloseRequested,
|
|
||||||
..
|
|
||||||
} => *control_flow = ControlFlow::Exit,
|
|
||||||
//Resize
|
|
||||||
Event::WindowEvent {
|
|
||||||
event: event::WindowEvent::Resized(new_size),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
size = new_size;
|
|
||||||
|
|
||||||
swap_chain = gpu_device.create_swap_chain(
|
|
||||||
&surface,
|
|
||||||
&wgpu::SwapChainDescriptor {
|
|
||||||
usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
|
|
||||||
format: render_format,
|
|
||||||
width: size.width,
|
|
||||||
height: size.height,
|
|
||||||
//Immediate may cause tearing, change present_mode if this becomes a problem
|
|
||||||
present_mode: wgpu::PresentMode::Immediate,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
update_ortho_buffer(
|
|
||||||
size.width,
|
|
||||||
size.height,
|
|
||||||
&gpu_device,
|
|
||||||
&rect_resources.ortho.buffer,
|
|
||||||
&cmd_queue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
//Received Character
|
|
||||||
Event::WindowEvent {
|
|
||||||
event: event::WindowEvent::ReceivedCharacter(ch),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if let Err(e) = app_update::handle_new_char(&ch, &mut app_model) {
|
|
||||||
print_err(&e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//Keyboard Input
|
|
||||||
Event::WindowEvent {
|
|
||||||
event: event::WindowEvent::KeyboardInput { input, .. },
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if let Some(virtual_keycode) = input.virtual_keycode {
|
|
||||||
if let Some(ref mut ed_model) = app_model.ed_model_opt {
|
|
||||||
if ed_model.has_focus {
|
|
||||||
let keydown_res = keyboard_input::handle_keydown(
|
|
||||||
input.state,
|
|
||||||
virtual_keycode,
|
|
||||||
keyboard_modifiers,
|
|
||||||
&mut app_model,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Err(e) = keydown_res {
|
|
||||||
print_err(&e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//Modifiers Changed
|
|
||||||
Event::WindowEvent {
|
|
||||||
event: event::WindowEvent::ModifiersChanged(modifiers),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
keyboard_modifiers = modifiers;
|
|
||||||
}
|
|
||||||
Event::MainEventsCleared => window.request_redraw(),
|
|
||||||
Event::RedrawRequested { .. } => {
|
|
||||||
// Get a command encoder for the current frame
|
|
||||||
let mut encoder =
|
|
||||||
gpu_device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
||||||
label: Some("Redraw"),
|
|
||||||
});
|
|
||||||
|
|
||||||
let frame = swap_chain
|
|
||||||
.get_current_frame()
|
|
||||||
.expect("Failed to acquire next SwapChainFrame")
|
|
||||||
.output;
|
|
||||||
|
|
||||||
if let Some(ed_model) = &app_model.ed_model_opt {
|
|
||||||
//TODO don't pass invisible lines
|
|
||||||
queue_editor_text(
|
|
||||||
&size,
|
|
||||||
&ed_model.text_buf.all_lines(&arena),
|
|
||||||
ed_model.caret_pos,
|
|
||||||
CODE_TXT_XY.into(),
|
|
||||||
&mut glyph_brush,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// queue_no_file_text(&size, NOTHING_OPENED, CODE_TXT_XY.into(), &mut glyph_brush);
|
|
||||||
|
|
||||||
let mut pool = Pool::with_capacity(12);
|
|
||||||
let mut var_store = VarStore::default();
|
|
||||||
let dep_idents = MutMap::default();
|
|
||||||
let mut module_ids = ModuleIds::default();
|
|
||||||
let exposed_ident_ids = IdentIds::default();
|
|
||||||
|
|
||||||
let home = module_ids.get_or_insert(&"Home".into());
|
|
||||||
|
|
||||||
let mut env = crate::lang::expr::Env::new(
|
|
||||||
home,
|
|
||||||
&arena,
|
|
||||||
&mut pool,
|
|
||||||
&mut var_store,
|
|
||||||
dep_idents,
|
|
||||||
&module_ids,
|
|
||||||
exposed_ident_ids,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut scope = Scope::new(home, env.pool, env.var_store);
|
|
||||||
|
|
||||||
let region = Region::new(0, 0, 0, 0);
|
|
||||||
|
|
||||||
let (expr2, _) = crate::lang::expr::str_to_expr2(
|
|
||||||
&arena, "True", &mut env, &mut scope, region,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
render::render_expr2(
|
|
||||||
&mut env,
|
|
||||||
&size,
|
|
||||||
&expr2,
|
|
||||||
CODE_TXT_XY.into(),
|
|
||||||
&mut glyph_brush,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
rects_arena.reset();
|
|
||||||
|
|
||||||
match draw_all_rects(
|
|
||||||
&app_model.ed_model_opt,
|
|
||||||
&rects_arena,
|
|
||||||
&mut encoder,
|
|
||||||
&frame.view,
|
|
||||||
&gpu_device,
|
|
||||||
&rect_resources,
|
|
||||||
) {
|
|
||||||
Ok(()) => (),
|
|
||||||
Err(e) => {
|
|
||||||
print_err(&e);
|
|
||||||
begin_render_pass(&mut encoder, &frame.view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw all text
|
|
||||||
glyph_brush
|
|
||||||
.draw_queued(
|
|
||||||
&gpu_device,
|
|
||||||
&mut staging_belt,
|
|
||||||
&mut encoder,
|
|
||||||
&frame.view,
|
|
||||||
size.width,
|
|
||||||
size.height,
|
|
||||||
)
|
|
||||||
.expect("Draw queued");
|
|
||||||
|
|
||||||
staging_belt.finish();
|
|
||||||
cmd_queue.submit(Some(encoder.finish()));
|
|
||||||
|
|
||||||
// Recall unused staging buffers
|
|
||||||
use futures::task::SpawnExt;
|
|
||||||
|
|
||||||
local_spawner
|
|
||||||
.spawn(staging_belt.recall())
|
|
||||||
.expect("Recall staging belt");
|
|
||||||
|
|
||||||
local_pool.run_until_stalled();
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
*control_flow = winit::event_loop::ControlFlow::Wait;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_all_rects(
|
|
||||||
ed_model_opt: &Option<EdModel>,
|
|
||||||
arena: &Bump,
|
|
||||||
encoder: &mut CommandEncoder,
|
|
||||||
texture_view: &TextureView,
|
|
||||||
gpu_device: &wgpu::Device,
|
|
||||||
rect_resources: &RectResources,
|
|
||||||
) -> EdResult<()> {
|
|
||||||
if let Some(ed_model) = ed_model_opt {
|
|
||||||
let all_rects = ed_view::create_ed_rects(ed_model, arena)?;
|
|
||||||
|
|
||||||
let rect_buffers = create_rect_buffers(gpu_device, encoder, &all_rects);
|
|
||||||
|
|
||||||
let mut render_pass = begin_render_pass(encoder, texture_view);
|
|
||||||
|
|
||||||
render_pass.set_pipeline(&rect_resources.pipeline);
|
|
||||||
render_pass.set_bind_group(0, &rect_resources.ortho.bind_group, &[]);
|
|
||||||
render_pass.set_vertex_buffer(0, rect_buffers.vertex_buffer.slice(..));
|
|
||||||
render_pass.set_index_buffer(rect_buffers.index_buffer.slice(..));
|
|
||||||
render_pass.draw_indexed(0..rect_buffers.num_rects, 0, 0..1);
|
|
||||||
} else {
|
|
||||||
// need to begin render pass to clear screen
|
|
||||||
begin_render_pass(encoder, texture_view);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn begin_render_pass<'a>(
|
|
||||||
encoder: &'a mut CommandEncoder,
|
|
||||||
texture_view: &'a TextureView,
|
|
||||||
) -> RenderPass<'a> {
|
|
||||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
||||||
color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor {
|
|
||||||
attachment: texture_view,
|
|
||||||
resolve_target: None,
|
|
||||||
ops: wgpu::Operations {
|
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
|
||||||
r: 0.1,
|
|
||||||
g: 0.2,
|
|
||||||
b: 0.3,
|
|
||||||
a: 1.0,
|
|
||||||
}),
|
|
||||||
store: true,
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
depth_stencil_attachment: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns bounding boxes for every glyph
|
|
||||||
fn queue_editor_text(
|
|
||||||
size: &PhysicalSize<u32>,
|
|
||||||
editor_lines: &str,
|
|
||||||
caret_pos: Position,
|
|
||||||
code_coords: Vector2<f32>,
|
|
||||||
glyph_brush: &mut GlyphBrush<()>,
|
|
||||||
) {
|
|
||||||
let area_bounds = (size.width as f32, size.height as f32).into();
|
|
||||||
|
|
||||||
let code_text = Text {
|
|
||||||
position: code_coords,
|
|
||||||
area_bounds,
|
|
||||||
color: CODE_COLOR.into(),
|
|
||||||
text: editor_lines,
|
|
||||||
size: CODE_FONT_SIZE,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let s = format!("Ln {}, Col {}", caret_pos.line, caret_pos.column);
|
|
||||||
let text = s.as_str();
|
|
||||||
|
|
||||||
let caret_pos_label = Text {
|
|
||||||
position: ((size.width as f32) - 150.0, (size.height as f32) - 40.0).into(),
|
|
||||||
area_bounds,
|
|
||||||
color: TXT_COLOR.into(),
|
|
||||||
text,
|
|
||||||
size: 25.0,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
queue_text_draw(&caret_pos_label, glyph_brush);
|
|
||||||
|
|
||||||
queue_code_text_draw(&code_text, glyph_brush);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _queue_no_file_text(
|
|
||||||
size: &PhysicalSize<u32>,
|
|
||||||
text: &str,
|
|
||||||
text_coords: Vector2<f32>,
|
|
||||||
glyph_brush: &mut GlyphBrush<()>,
|
|
||||||
) {
|
|
||||||
let area_bounds = (size.width as f32, size.height as f32).into();
|
|
||||||
|
|
||||||
let code_text = Text {
|
|
||||||
position: text_coords,
|
|
||||||
area_bounds,
|
|
||||||
color: CODE_COLOR.into(),
|
|
||||||
text,
|
|
||||||
size: CODE_FONT_SIZE,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
queue_text_draw(&code_text, glyph_brush);
|
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,20 +0,0 @@
|
||||||
use snafu::{Backtrace, ErrorCompat, Snafu};
|
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
#[snafu(visibility(pub))]
|
|
||||||
pub enum UIError {
|
|
||||||
#[snafu(display(
|
|
||||||
"OutOfBounds: index {} was out of bounds for {} with length {}.",
|
|
||||||
index,
|
|
||||||
collection_name,
|
|
||||||
len
|
|
||||||
))]
|
|
||||||
OutOfBounds {
|
|
||||||
index: usize,
|
|
||||||
collection_name: String,
|
|
||||||
len: usize,
|
|
||||||
backtrace: Backtrace,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type UIResult<T, E = UIError> = std::result::Result<T, E>;
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod error;
|
pub mod ui_error;
|
||||||
pub mod model;
|
pub mod colors;
|
||||||
pub mod update;
|
pub mod text;
|
||||||
|
mod util;
|
|
@ -1 +0,0 @@
|
||||||
pub mod text;
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod selectable_text_m;
|
|
||||||
pub mod selection;
|
|
||||||
pub mod txt_pos;
|
|
|
@ -1,48 +0,0 @@
|
||||||
|
|
||||||
use crate::ui::error::UIResult;
|
|
||||||
use super::txt_pos::TxtPos;
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
impl std::fmt::Display for RawSelection {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"RawSelection: start_pos: line:{} col:{}, end_pos: line:{} col:{}",
|
|
||||||
self.start_pos.line, self.start_pos.column, self.end_pos.line, self.end_pos.column
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trait SelectableLines {
|
|
||||||
fn get_line(&self, line_nr: usize) -> UIResult<&str>;
|
|
||||||
|
|
||||||
fn line_len(&self, line_nr: usize) -> UIResult<usize>;
|
|
||||||
|
|
||||||
fn nr_of_lines(&self) -> usize;
|
|
||||||
|
|
||||||
fn nr_of_chars(&self) -> usize;
|
|
||||||
|
|
||||||
// TODO use pool allocation here
|
|
||||||
fn all_lines<'a>(&self, arena: &'a Bump) -> BumpString<'a>;
|
|
||||||
|
|
||||||
fn last_TxtPos(&self) -> TxtPos;
|
|
||||||
|
|
||||||
fn get_selection(&self, raw_sel: RawSelection) -> UIResult<&str>;
|
|
||||||
}
|
|
||||||
|
|
||||||
trait MutSelectableLines {
|
|
||||||
fn insert_char(&mut self, caret_pos: TxtPos, new_char: &char) -> UIResult<()>;
|
|
||||||
|
|
||||||
fn insert_str(&mut self, caret_pos: TxtPos, new_str: &str) -> UIResult<()>;
|
|
||||||
|
|
||||||
fn pop_char(&mut self, caret_pos: TxtPos);
|
|
||||||
|
|
||||||
fn del_selection(&mut self, raw_sel: RawSelection) -> UIResult<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SelectableText {
|
|
||||||
pub lines: SelectableLines,
|
|
||||||
pub caret_pos: TxtPos,
|
|
||||||
pub selection_opt: Option<RawSelection>,
|
|
||||||
}
|
|
|
@ -1,76 +1,31 @@
|
||||||
// Adapted from https://github.com/cessen/ropey by Nathan Vegdahl, licensed under the MIT license
|
// Adapted from https://github.com/cessen/ropey by Nathan Vegdahl, licensed under the MIT license
|
||||||
|
|
||||||
use crate::error::EdError::{FileOpenFailed, TextBufReadFailed};
|
use crate::ui::ui_error::UIError::{FileOpenFailed, TextBufReadFailed};
|
||||||
use crate::error::EdResult;
|
use crate::ui::ui_error::UIResult;
|
||||||
use crate::error::OutOfBounds;
|
use crate::ui::ui_error::OutOfBounds;
|
||||||
use crate::mvc::ed_model::{Position, RawSelection};
|
use crate::ui::text::text_pos::{TextPos};
|
||||||
use crate::selection::validate_selection;
|
use crate::ui::text::selection::{ValidSelection};
|
||||||
|
use crate::ui::text::caret_w_select::{CaretWSelect};
|
||||||
|
use crate::ui::text::lines::{Lines, SelectableLines, MutSelectableLines};
|
||||||
use bumpalo::collections::String as BumpString;
|
use bumpalo::collections::String as BumpString;
|
||||||
use bumpalo::Bump;
|
use bumpalo::Bump;
|
||||||
use ropey::Rope;
|
use ropey::Rope;
|
||||||
use snafu::{ensure, OptionExt};
|
use snafu::{ensure};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub struct TextBuffer {
|
pub struct BigSelectableText {
|
||||||
|
pub caret_w_select: CaretWSelect,
|
||||||
pub text_rope: Rope,
|
pub text_rope: Rope,
|
||||||
pub path_str: String,
|
pub path_str: String,
|
||||||
pub arena: Bump,
|
pub arena: Bump,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextBuffer {
|
impl BigSelectableText {
|
||||||
pub fn insert_char(&mut self, caret_pos: Position, new_char: &char) -> EdResult<()> {
|
|
||||||
self.insert_str(caret_pos, &new_char.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_str(&mut self, caret_pos: Position, new_str: &str) -> EdResult<()> {
|
fn check_bounds(&self, char_indx: usize) -> UIResult<()> {
|
||||||
let char_indx = self.pos_to_char_indx(caret_pos);
|
|
||||||
|
|
||||||
self.check_bounds(char_indx)?;
|
|
||||||
|
|
||||||
self.text_rope.insert(char_indx, new_str);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pop_char(&mut self, caret_pos: Position) {
|
|
||||||
let char_indx = self.pos_to_char_indx(caret_pos);
|
|
||||||
|
|
||||||
if (char_indx > 0) && char_indx <= self.text_rope.len_chars() {
|
|
||||||
self.text_rope.remove((char_indx - 1)..char_indx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn del_selection(&mut self, raw_sel: RawSelection) -> EdResult<()> {
|
|
||||||
let (start_char_indx, end_char_indx) = self.sel_to_tup(raw_sel)?;
|
|
||||||
|
|
||||||
self.check_bounds(end_char_indx)?;
|
|
||||||
|
|
||||||
self.text_rope.remove(start_char_indx..end_char_indx);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_selection(&self, raw_sel: RawSelection) -> EdResult<&str> {
|
|
||||||
let (start_char_indx, end_char_indx) = self.sel_to_tup(raw_sel)?;
|
|
||||||
|
|
||||||
self.check_bounds(end_char_indx)?;
|
|
||||||
|
|
||||||
let rope_slice = self.text_rope.slice(start_char_indx..end_char_indx);
|
|
||||||
|
|
||||||
if let Some(line_str_ref) = rope_slice.as_str() {
|
|
||||||
Ok(line_str_ref)
|
|
||||||
} else {
|
|
||||||
// happens very rarely
|
|
||||||
let line_str = rope_slice.chunks().collect::<String>();
|
|
||||||
let arena_str_ref = self.arena.alloc(line_str);
|
|
||||||
Ok(arena_str_ref)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_bounds(&self, char_indx: usize) -> EdResult<()> {
|
|
||||||
ensure!(
|
ensure!(
|
||||||
char_indx <= self.text_rope.len_chars(),
|
char_indx <= self.text_rope.len_chars(),
|
||||||
OutOfBounds {
|
OutOfBounds {
|
||||||
|
@ -83,46 +38,68 @@ impl TextBuffer {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn line(&self, line_nr: usize) -> Option<&str> {
|
fn pos_to_char_indx(&self, pos: TextPos) -> usize {
|
||||||
if line_nr < self.nr_of_lines() {
|
self.text_rope.line_to_char(pos.line) + pos.column
|
||||||
|
}
|
||||||
|
|
||||||
|
fn char_indx_to_pos(&self, char_indx: usize) -> TextPos {
|
||||||
|
let line = self.text_rope.char_to_line(char_indx);
|
||||||
|
|
||||||
|
let char_idx_line_start = self.pos_to_char_indx(TextPos { line, column: 0 });
|
||||||
|
|
||||||
|
let column = char_indx - char_idx_line_start;
|
||||||
|
|
||||||
|
TextPos { line, column }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sel_to_tup(&self, val_sel: ValidSelection) -> UIResult<(usize, usize)> {
|
||||||
|
let start_char_indx = self.pos_to_char_indx(val_sel.start_pos);
|
||||||
|
let end_char_indx = self.pos_to_char_indx(val_sel.end_pos);
|
||||||
|
|
||||||
|
Ok((start_char_indx, end_char_indx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Lines for BigSelectableText {
|
||||||
|
fn get_line(&self, line_nr: usize) -> UIResult<&str> {
|
||||||
|
ensure!(
|
||||||
|
line_nr < self.nr_of_lines(),
|
||||||
|
OutOfBounds {
|
||||||
|
index: line_nr,
|
||||||
|
collection_name: "BigSelectableText",
|
||||||
|
len: self.nr_of_lines(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
let rope_slice = self.text_rope.line(line_nr);
|
let rope_slice = self.text_rope.line(line_nr);
|
||||||
|
|
||||||
if let Some(line_str_ref) = rope_slice.as_str() {
|
if let Some(line_str_ref) = rope_slice.as_str() {
|
||||||
Some(line_str_ref)
|
Ok(line_str_ref)
|
||||||
} else {
|
} else {
|
||||||
// happens very rarely
|
// happens very rarely
|
||||||
let line_str = rope_slice.chunks().collect::<String>();
|
let line_str = rope_slice.chunks().collect::<String>();
|
||||||
let arena_str_ref = self.arena.alloc(line_str);
|
let arena_str_ref = self.arena.alloc(line_str);
|
||||||
Some(arena_str_ref)
|
Ok(arena_str_ref)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn line_len(&self, line_nr: usize) -> Option<usize> {
|
|
||||||
self.line(line_nr).map(|line| line.len())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn line_len_res(&self, line_nr: usize) -> EdResult<usize> {
|
fn line_len(&self, line_nr: usize) -> UIResult<usize> {
|
||||||
self.line_len(line_nr).context(OutOfBounds {
|
self.get_line(line_nr).map(|line| line.len())
|
||||||
index: line_nr,
|
|
||||||
collection_name: "Rope",
|
|
||||||
len: self.text_rope.len_lines(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nr_of_lines(&self) -> usize {
|
fn nr_of_lines(&self) -> usize {
|
||||||
self.text_rope.len_lines()
|
self.text_rope.len_lines()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nr_of_chars(&self) -> usize {
|
fn nr_of_chars(&self) -> usize {
|
||||||
self.text_rope.len_chars()
|
self.text_rope.len_chars()
|
||||||
}
|
}
|
||||||
|
|
||||||
// expensive function, don't use it if it can be done with a specialized, more efficient function
|
|
||||||
// TODO use pool allocation here
|
// TODO use pool allocation here
|
||||||
pub fn all_lines<'a>(&self, arena: &'a Bump) -> BumpString<'a> {
|
// expensive function, don't use it if it can be done with a specialized, more efficient function
|
||||||
|
fn all_lines<'a>(&self, arena: &'a Bump) -> BumpString<'a> {
|
||||||
let mut lines = BumpString::with_capacity_in(self.text_rope.len_chars(), arena);
|
let mut lines = BumpString::with_capacity_in(self.text_rope.len_chars(), arena);
|
||||||
|
|
||||||
for line in self.text_rope.lines() {
|
for line in self.text_rope.lines() {
|
||||||
|
@ -131,40 +108,81 @@ impl TextBuffer {
|
||||||
|
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn pos_to_char_indx(&self, pos: Position) -> usize {
|
impl SelectableLines for BigSelectableText {
|
||||||
self.text_rope.line_to_char(pos.line) + pos.column
|
fn get_selection(&self) -> UIResult<Option<&str>> {
|
||||||
|
if let Some(val_sel) = self.caret_w_select.selection_opt {
|
||||||
|
let (start_char_indx, end_char_indx) = self.sel_to_tup(val_sel)?;
|
||||||
|
|
||||||
|
self.check_bounds(end_char_indx)?;
|
||||||
|
|
||||||
|
let rope_slice = self.text_rope.slice(start_char_indx..end_char_indx);
|
||||||
|
|
||||||
|
if let Some(line_str_ref) = rope_slice.as_str() {
|
||||||
|
Ok(Some(line_str_ref))
|
||||||
|
} else {
|
||||||
|
// happens very rarely
|
||||||
|
let line_str = rope_slice.chunks().collect::<String>();
|
||||||
|
let arena_str_ref = self.arena.alloc(line_str);
|
||||||
|
Ok(Some(arena_str_ref))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn char_indx_to_pos(&self, char_indx: usize) -> Position {
|
fn last_text_pos(&self) -> TextPos {
|
||||||
let line = self.text_rope.char_to_line(char_indx);
|
|
||||||
|
|
||||||
let char_idx_line_start = self.pos_to_char_indx(Position { line, column: 0 });
|
|
||||||
|
|
||||||
let column = char_indx - char_idx_line_start;
|
|
||||||
|
|
||||||
Position { line, column }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sel_to_tup(&self, raw_sel: RawSelection) -> EdResult<(usize, usize)> {
|
|
||||||
let valid_sel = validate_selection(raw_sel)?;
|
|
||||||
let start_char_indx = self.pos_to_char_indx(valid_sel.selection.start_pos);
|
|
||||||
let end_char_indx = self.pos_to_char_indx(valid_sel.selection.end_pos);
|
|
||||||
|
|
||||||
Ok((start_char_indx, end_char_indx))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn last_position(&self) -> Position {
|
|
||||||
self.char_indx_to_pos(self.nr_of_chars())
|
self.char_indx_to_pos(self.nr_of_chars())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_path(path: &Path) -> EdResult<TextBuffer> {
|
impl MutSelectableLines for BigSelectableText {
|
||||||
|
fn insert_char(&mut self, caret_pos: TextPos, new_char: &char) -> UIResult<()> {
|
||||||
|
self.insert_str(caret_pos, &new_char.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_str(&mut self, caret_pos: TextPos, new_str: &str) -> UIResult<()> {
|
||||||
|
let char_indx = self.pos_to_char_indx(caret_pos);
|
||||||
|
|
||||||
|
self.check_bounds(char_indx)?;
|
||||||
|
|
||||||
|
self.text_rope.insert(char_indx, new_str);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_char(&mut self, caret_pos: TextPos) {
|
||||||
|
let char_indx = self.pos_to_char_indx(caret_pos);
|
||||||
|
|
||||||
|
if (char_indx > 0) && char_indx <= self.text_rope.len_chars() {
|
||||||
|
self.text_rope.remove((char_indx - 1)..char_indx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn del_selection(&mut self) -> UIResult<()> {
|
||||||
|
if let Some(valid_sel) = self.caret_w_select.selection_opt {
|
||||||
|
let (start_char_indx, end_char_indx) = self.sel_to_tup(valid_sel)?;
|
||||||
|
|
||||||
|
self.check_bounds(end_char_indx)?;
|
||||||
|
|
||||||
|
self.text_rope.remove(start_char_indx..end_char_indx);
|
||||||
|
|
||||||
|
// TODO adjust caret pos aftere delete
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_path(path: &Path) -> UIResult<BigSelectableText> {
|
||||||
|
let caret_w_select = CaretWSelect::default();
|
||||||
let text_rope = rope_from_path(path)?;
|
let text_rope = rope_from_path(path)?;
|
||||||
let path_str = path_to_string(path);
|
let path_str = path_to_string(path);
|
||||||
let arena = Bump::new();
|
let arena = Bump::new();
|
||||||
|
|
||||||
Ok(TextBuffer {
|
Ok(BigSelectableText {
|
||||||
|
caret_w_select,
|
||||||
text_rope,
|
text_rope,
|
||||||
path_str,
|
path_str,
|
||||||
arena,
|
arena,
|
||||||
|
@ -178,7 +196,7 @@ fn path_to_string(path: &Path) -> String {
|
||||||
path_str
|
path_str
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rope_from_path(path: &Path) -> EdResult<Rope> {
|
fn rope_from_path(path: &Path) -> UIResult<Rope> {
|
||||||
match File::open(path) {
|
match File::open(path) {
|
||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
let buf_reader = &mut io::BufReader::new(file);
|
let buf_reader = &mut io::BufReader::new(file);
|
||||||
|
@ -197,9 +215,11 @@ fn rope_from_path(path: &Path) -> EdResult<Rope> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for TextBuffer {
|
// need to explicitly omit arena
|
||||||
|
impl fmt::Debug for BigSelectableText {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct("TextBuffer")
|
f.debug_struct("BigSelectableText")
|
||||||
|
.field("caret_w_select", &self.caret_w_select)
|
||||||
.field("text_rope", &self.text_rope)
|
.field("text_rope", &self.text_rope)
|
||||||
.field("path_str", &self.path_str)
|
.field("path_str", &self.path_str)
|
||||||
.finish()
|
.finish()
|
108
editor/src/ui/text/caret_w_select.rs
Normal file
108
editor/src/ui/text/caret_w_select.rs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
|
||||||
|
use crate::ui::ui_error::UIResult;
|
||||||
|
use super::text_pos::TextPos;
|
||||||
|
use super::selection::{ValidSelection};
|
||||||
|
use winit::event::{ModifiersState};
|
||||||
|
use super::selection::validate_selection;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CaretWSelect {
|
||||||
|
pub caret_pos: TextPos,
|
||||||
|
pub selection_opt: Option<ValidSelection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn mk_some_sel(start_pos: TextPos, end_pos: TextPos) -> UIResult<Option<ValidSelection>> {
|
||||||
|
Ok(
|
||||||
|
Some(
|
||||||
|
validate_selection(start_pos, end_pos)?
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CaretWSelect {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
caret_pos: TextPos {
|
||||||
|
line: 0,
|
||||||
|
column: 0
|
||||||
|
},
|
||||||
|
selection_opt: None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CaretWSelect {
|
||||||
|
|
||||||
|
pub fn move_caret_w_mods(&mut self, new_pos: TextPos, mods: &ModifiersState) -> UIResult<()> {
|
||||||
|
let caret_pos = self.caret_pos;
|
||||||
|
|
||||||
|
// one does not simply move the caret
|
||||||
|
let valid_sel_opt =
|
||||||
|
if new_pos != caret_pos {
|
||||||
|
if mods.shift() {
|
||||||
|
if let Some(old_sel) = self.selection_opt {
|
||||||
|
if new_pos < old_sel.start_pos {
|
||||||
|
if caret_pos > old_sel.start_pos {
|
||||||
|
mk_some_sel(
|
||||||
|
new_pos,
|
||||||
|
old_sel.start_pos
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
mk_some_sel(
|
||||||
|
new_pos,
|
||||||
|
old_sel.end_pos
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
} else if new_pos > old_sel.end_pos {
|
||||||
|
if caret_pos < old_sel.end_pos {
|
||||||
|
mk_some_sel(
|
||||||
|
old_sel.end_pos,
|
||||||
|
new_pos
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
mk_some_sel(
|
||||||
|
old_sel.start_pos,
|
||||||
|
new_pos
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
} else if new_pos > caret_pos {
|
||||||
|
mk_some_sel(
|
||||||
|
new_pos,
|
||||||
|
old_sel.end_pos
|
||||||
|
)?
|
||||||
|
} else if new_pos < caret_pos {
|
||||||
|
mk_some_sel(
|
||||||
|
old_sel.start_pos,
|
||||||
|
new_pos
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
// TODO should this return none?
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else if new_pos < self.caret_pos {
|
||||||
|
mk_some_sel(
|
||||||
|
new_pos,
|
||||||
|
caret_pos
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
mk_some_sel(
|
||||||
|
caret_pos,
|
||||||
|
new_pos
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.selection_opt
|
||||||
|
};
|
||||||
|
|
||||||
|
self.caret_pos = new_pos;
|
||||||
|
self.selection_opt = valid_sel_opt;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
44
editor/src/ui/text/lines.rs
Normal file
44
editor/src/ui/text/lines.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// Adapted from https://github.com/cessen/ropey by Nathan Vegdahl, licensed under the MIT license
|
||||||
|
|
||||||
|
use crate::ui::ui_error::UIError::{FileOpenFailed, TextBufReadFailed};
|
||||||
|
use crate::ui::ui_error::UIResult;
|
||||||
|
use crate::ui::ui_error::OutOfBounds;
|
||||||
|
use crate::ui::text::text_pos::{TextPos};
|
||||||
|
use crate::ui::text::selection::{ValidSelection};
|
||||||
|
use bumpalo::collections::String as BumpString;
|
||||||
|
use bumpalo::Bump;
|
||||||
|
use ropey::Rope;
|
||||||
|
use snafu::{ensure, OptionExt};
|
||||||
|
use std::fmt;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub trait Lines {
|
||||||
|
fn get_line(&self, line_nr: usize) -> UIResult<&str>;
|
||||||
|
|
||||||
|
fn line_len(&self, line_nr: usize) -> UIResult<usize>;
|
||||||
|
|
||||||
|
fn nr_of_lines(&self) -> usize;
|
||||||
|
|
||||||
|
fn nr_of_chars(&self) -> usize;
|
||||||
|
|
||||||
|
// TODO use pool allocation here
|
||||||
|
fn all_lines<'a>(&self, arena: &'a Bump) -> BumpString<'a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait SelectableLines {
|
||||||
|
fn get_selection(&self) -> UIResult<Option<&str>>;
|
||||||
|
|
||||||
|
fn last_text_pos(&self) -> TextPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MutSelectableLines {
|
||||||
|
fn insert_char(&mut self, caret_pos: TextPos, new_char: &char) -> UIResult<()>;
|
||||||
|
|
||||||
|
fn insert_str(&mut self, caret_pos: TextPos, new_str: &str) -> UIResult<()>;
|
||||||
|
|
||||||
|
fn pop_char(&mut self, caret_pos: TextPos);
|
||||||
|
|
||||||
|
fn del_selection(&mut self) -> UIResult<()>;
|
||||||
|
}
|
5
editor/src/ui/text/mod.rs
Normal file
5
editor/src/ui/text/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod caret_w_select;
|
||||||
|
pub mod selection;
|
||||||
|
pub mod text_pos;
|
||||||
|
pub mod big_selectable_text;
|
||||||
|
pub mod lines;
|
|
@ -1,21 +1,48 @@
|
||||||
use crate::ui::error::{UIResult, InvalidSelection};
|
use crate::ui::ui_error::{UIResult, InvalidSelection};
|
||||||
|
use crate::ui::colors;
|
||||||
|
use super::text_pos::TextPos;
|
||||||
|
use super::lines::Lines;
|
||||||
use bumpalo::collections::Vec as BumpVec;
|
use bumpalo::collections::Vec as BumpVec;
|
||||||
use bumpalo::Bump;
|
|
||||||
use snafu::ensure;
|
use snafu::ensure;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct RawSelection {
|
pub struct RawSelection {
|
||||||
pub start_pos: TxtPos,
|
pub start_pos: TextPos,
|
||||||
pub end_pos: TxtPos,
|
pub end_pos: TextPos,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawSelection {
|
||||||
|
pub fn make_raw_sel(start_pos: TextPos, end_pos: TextPos) -> RawSelection {
|
||||||
|
RawSelection {
|
||||||
|
start_pos,
|
||||||
|
end_pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RawSelection {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"RawSelection: start_pos: line:{} col:{}, end_pos: line:{} col:{}",
|
||||||
|
self.start_pos.line, self.start_pos.column, self.end_pos.line, self.end_pos.column
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//using the "parse don't validate" pattern
|
//using the "parse don't validate" pattern
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct ValidSelection {
|
pub struct ValidSelection {
|
||||||
pub selection: RawSelection,
|
pub start_pos: TextPos,
|
||||||
|
pub end_pos: TextPos,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_selection(selection: RawSelection) -> EdResult<ValidSelection> {
|
pub fn validate_raw_sel(raw_sel: RawSelection) -> UIResult<ValidSelection> {
|
||||||
let RawSelection { start_pos, end_pos } = selection;
|
validate_selection(raw_sel.start_pos, raw_sel.end_pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_selection(start_pos: TextPos, end_pos: TextPos) -> UIResult<ValidSelection> {
|
||||||
|
|
||||||
ensure!(
|
ensure!(
|
||||||
start_pos.line <= end_pos.line,
|
start_pos.line <= end_pos.line,
|
||||||
|
@ -39,18 +66,22 @@ pub fn validate_selection(selection: RawSelection) -> EdResult<ValidSelection> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(ValidSelection {
|
Ok(ValidSelection {
|
||||||
selection: RawSelection { start_pos, end_pos },
|
start_pos, end_pos,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
use bumpalo::Bump;
|
||||||
|
use crate::graphics::primitives::rect::Rect;
|
||||||
|
use crate::ui::text::lines::Lines;
|
||||||
|
|
||||||
pub fn create_selection_rects<'a>(
|
pub fn create_selection_rects<'a>(
|
||||||
raw_sel: RawSelection,
|
valid_sel: ValidSelection,
|
||||||
text_buf: &TextBuffer,
|
lines: &dyn Lines,
|
||||||
glyph_dim_rect: &Rect,
|
glyph_dim_rect: &Rect,
|
||||||
arena: &'a Bump,
|
arena: &'a Bump,
|
||||||
) -> EdResult<BumpVec<'a, Rect>> {
|
) -> UIResult<BumpVec<'a, Rect>> {
|
||||||
let valid_sel = validate_selection(raw_sel)?;
|
let ValidSelection { start_pos, end_pos } = valid_sel;
|
||||||
let RawSelection { start_pos, end_pos } = valid_sel.selection;
|
|
||||||
|
|
||||||
let mut all_rects: BumpVec<Rect> = BumpVec::new_in(arena);
|
let mut all_rects: BumpVec<Rect> = BumpVec::new_in(arena);
|
||||||
|
|
||||||
|
@ -73,7 +104,7 @@ pub fn create_selection_rects<'a>(
|
||||||
Ok(all_rects)
|
Ok(all_rects)
|
||||||
} else {
|
} else {
|
||||||
// first line
|
// first line
|
||||||
let end_col = text_buf.line_len_res(start_pos.line)?;
|
let end_col = lines.line_len(start_pos.line)?;
|
||||||
let width = ((end_col as f32) * glyph_dim_rect.width)
|
let width = ((end_col as f32) * glyph_dim_rect.width)
|
||||||
- ((start_pos.column as f32) * glyph_dim_rect.width);
|
- ((start_pos.column as f32) * glyph_dim_rect.width);
|
||||||
|
|
||||||
|
@ -91,7 +122,7 @@ pub fn create_selection_rects<'a>(
|
||||||
let first_mid_line = start_pos.line + 1;
|
let first_mid_line = start_pos.line + 1;
|
||||||
|
|
||||||
for i in first_mid_line..(first_mid_line + nr_mid_lines) {
|
for i in first_mid_line..(first_mid_line + nr_mid_lines) {
|
||||||
let mid_line_len = text_buf.line_len_res(i)?;
|
let mid_line_len = lines.line_len(i)?;
|
||||||
|
|
||||||
let width = (mid_line_len as f32) * glyph_dim_rect.width;
|
let width = (mid_line_len as f32) * glyph_dim_rect.width;
|
||||||
|
|
||||||
|
@ -126,13 +157,14 @@ pub fn create_selection_rects<'a>(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test_selection {
|
pub mod test_selection {
|
||||||
use crate::error::{EdResult, OutOfBounds};
|
use crate::ui::ui_error::{UIResult, OutOfBounds};
|
||||||
use crate::mvc::ed_model::{Position, RawSelection};
|
use crate::ui::text::text_pos::TextPos;
|
||||||
use crate::mvc::ed_update::{
|
use crate::ui::text::selection::{ValidSelection, validate_selection};
|
||||||
|
use crate::editor::mvc::ed_update::{
|
||||||
move_caret_down, move_caret_left, move_caret_right, move_caret_up, MoveCaretFun,
|
move_caret_down, move_caret_left, move_caret_right, move_caret_up, MoveCaretFun,
|
||||||
};
|
};
|
||||||
use crate::text_buffer::TextBuffer;
|
use crate::ui::text::big_selectable_text::BigSelectableText;
|
||||||
use crate::vec_result::get_res;
|
use crate::ui::util::slice_get;
|
||||||
use core::cmp::Ordering;
|
use core::cmp::Ordering;
|
||||||
use pest::Parser;
|
use pest::Parser;
|
||||||
use ropey::Rope;
|
use ropey::Rope;
|
||||||
|
@ -146,14 +178,14 @@ pub mod test_selection {
|
||||||
|
|
||||||
// show selection and caret position as symbols in lines for easy testing
|
// show selection and caret position as symbols in lines for easy testing
|
||||||
pub fn convert_selection_to_dsl(
|
pub fn convert_selection_to_dsl(
|
||||||
raw_sel_opt: Option<RawSelection>,
|
selection_opt: Option<ValidSelection>,
|
||||||
caret_pos: Position,
|
caret_pos: TextPos,
|
||||||
lines: &mut [String],
|
lines: &mut [String],
|
||||||
) -> EdResult<&[String]> {
|
) -> UIResult<&[String]> {
|
||||||
if let Some(raw_sel) = raw_sel_opt {
|
if let Some(sel) = selection_opt {
|
||||||
let mut to_insert = vec![
|
let mut to_insert = vec![
|
||||||
(raw_sel.start_pos, '['),
|
(sel.start_pos, '['),
|
||||||
(raw_sel.end_pos, ']'),
|
(sel.end_pos, ']'),
|
||||||
(caret_pos, '|'),
|
(caret_pos, '|'),
|
||||||
];
|
];
|
||||||
let symbol_map: HashMap<char, usize> =
|
let symbol_map: HashMap<char, usize> =
|
||||||
|
@ -191,7 +223,7 @@ pub mod test_selection {
|
||||||
Ok(lines)
|
Ok(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_at_pos(lines: &mut [String], pos: Position, insert_char: char) -> EdResult<()> {
|
fn insert_at_pos(lines: &mut [String], pos: TextPos, insert_char: char) -> UIResult<()> {
|
||||||
let line = get_mut_res(pos.line, lines)?;
|
let line = get_mut_res(pos.line, lines)?;
|
||||||
line.insert(pos.column, insert_char);
|
line.insert(pos.column, insert_char);
|
||||||
|
|
||||||
|
@ -202,7 +234,7 @@ pub mod test_selection {
|
||||||
fn get_mut_res<T>(
|
fn get_mut_res<T>(
|
||||||
index: usize,
|
index: usize,
|
||||||
vec: &mut [T],
|
vec: &mut [T],
|
||||||
) -> EdResult<&mut <usize as SliceIndex<[T]>>::Output> {
|
) -> UIResult<&mut <usize as SliceIndex<[T]>>::Output> {
|
||||||
let vec_len = vec.len();
|
let vec_len = vec.len();
|
||||||
|
|
||||||
let elt_ref = vec.get_mut(index).context(OutOfBounds {
|
let elt_ref = vec.get_mut(index).context(OutOfBounds {
|
||||||
|
@ -252,7 +284,7 @@ pub mod test_selection {
|
||||||
// Retrieve selection and position from formatted string
|
// Retrieve selection and position from formatted string
|
||||||
pub fn convert_dsl_to_selection(
|
pub fn convert_dsl_to_selection(
|
||||||
lines: &[String],
|
lines: &[String],
|
||||||
) -> Result<(Option<RawSelection>, Position), String> {
|
) -> Result<(Option<ValidSelection>, TextPos), String> {
|
||||||
let lines_str: String = lines.join("");
|
let lines_str: String = lines.join("");
|
||||||
|
|
||||||
let parsed = LineParser::parse(Rule::linesWithSelect, &lines_str)
|
let parsed = LineParser::parse(Rule::linesWithSelect, &lines_str)
|
||||||
|
@ -319,24 +351,28 @@ pub mod test_selection {
|
||||||
|
|
||||||
// Make sure return makes sense
|
// Make sure return makes sense
|
||||||
if let Some((line, column)) = caret_opt {
|
if let Some((line, column)) = caret_opt {
|
||||||
let caret_pos = Position { line, column };
|
let caret_pos = TextPos { line, column };
|
||||||
if sel_start_opt.is_none() && sel_end_opt.is_none() {
|
if sel_start_opt.is_none() && sel_end_opt.is_none() {
|
||||||
Ok((None, caret_pos))
|
Ok((None, caret_pos))
|
||||||
} else if let Some((start_line, start_column)) = sel_start_opt {
|
} else if let Some((start_line, start_column)) = sel_start_opt {
|
||||||
if let Some((end_line, end_column)) = sel_end_opt {
|
if let Some((end_line, end_column)) = sel_end_opt {
|
||||||
Ok((
|
Ok(
|
||||||
Some(RawSelection {
|
(
|
||||||
start_pos: Position {
|
Some(
|
||||||
|
validate_selection(
|
||||||
|
TextPos {
|
||||||
line: start_line,
|
line: start_line,
|
||||||
column: start_column,
|
column: start_column,
|
||||||
},
|
},
|
||||||
end_pos: Position {
|
TextPos {
|
||||||
line: end_line,
|
line: end_line,
|
||||||
column: end_column,
|
column: end_column,
|
||||||
},
|
},
|
||||||
}),
|
).unwrap()
|
||||||
caret_pos,
|
),
|
||||||
))
|
caret_pos
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Err("Selection end ']' was not found, but selection start '[' was. Bad input string.".to_owned())
|
Err("Selection end ']' was not found, but selection start '[' was. Bad input string.".to_owned())
|
||||||
}
|
}
|
|
@ -2,27 +2,27 @@
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct TxtPos {
|
pub struct TextPos {
|
||||||
pub line: usize,
|
pub line: usize,
|
||||||
pub column: usize,
|
pub column: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ord for TxtPos {
|
impl Ord for TextPos {
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
(self.line, self.column).cmp(&(other.line, other.column))
|
(self.line, self.column).cmp(&(other.line, other.column))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for TxtPos {
|
impl PartialOrd for TextPos {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
Some(self.cmp(other))
|
Some(self.cmp(other))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for TxtPos {
|
impl PartialEq for TextPos {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
(self.line, self.column) == (other.line, other.column)
|
(self.line, self.column) == (other.line, other.column)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for TxtPos {}
|
impl Eq for TextPos {}
|
36
editor/src/ui/ui_error.rs
Normal file
36
editor/src/ui/ui_error.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use snafu::{Backtrace, ErrorCompat, Snafu};
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
#[snafu(visibility(pub))]
|
||||||
|
pub enum UIError {
|
||||||
|
#[snafu(display(
|
||||||
|
"OutOfBounds: index {} was out of bounds for {} with length {}.",
|
||||||
|
index,
|
||||||
|
collection_name,
|
||||||
|
len
|
||||||
|
))]
|
||||||
|
OutOfBounds {
|
||||||
|
index: usize,
|
||||||
|
collection_name: String,
|
||||||
|
len: usize,
|
||||||
|
backtrace: Backtrace,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[snafu(display("InvalidSelection: {}.", err_msg))]
|
||||||
|
InvalidSelection {
|
||||||
|
err_msg: String,
|
||||||
|
backtrace: Backtrace,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[snafu(display(
|
||||||
|
"FileOpenFailed: failed to open file with path {} with the following error: {}.",
|
||||||
|
path_str,
|
||||||
|
err_msg
|
||||||
|
))]
|
||||||
|
FileOpenFailed { path_str: String, err_msg: String },
|
||||||
|
|
||||||
|
#[snafu(display("TextBufReadFailed: the file {} could be opened but we encountered the following error while trying to read it: {}.", path_str, err_msg))]
|
||||||
|
TextBufReadFailed { path_str: String, err_msg: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type UIResult<T, E = UIError> = std::result::Result<T, E>;
|
|
@ -1 +0,0 @@
|
||||||
pub mod SelectableTextU;
|
|
|
@ -1,62 +0,0 @@
|
||||||
impl SelectableText {
|
|
||||||
pub fn move_caret_w_mods(&mut self, new_pos: Position, mods: &ModifiersState) {
|
|
||||||
let caret_pos = self.caret_pos;
|
|
||||||
|
|
||||||
// one does not simply move the caret
|
|
||||||
if new_pos != caret_pos {
|
|
||||||
if mods.shift() {
|
|
||||||
if let Some(selection) = self.selection_opt {
|
|
||||||
if new_pos < selection.start_pos {
|
|
||||||
if caret_pos > selection.start_pos {
|
|
||||||
self.set_selection(
|
|
||||||
new_pos,
|
|
||||||
selection.start_pos
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
self.set_selection(
|
|
||||||
new_pos,
|
|
||||||
selection.end_pos
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if new_pos > selection.end_pos {
|
|
||||||
if caret_pos < selection.end_pos {
|
|
||||||
self.set_selection(
|
|
||||||
selection.end_pos,
|
|
||||||
new_pos
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
self.set_selection(
|
|
||||||
selection.start_pos,
|
|
||||||
new_pos
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if new_pos > caret_pos {
|
|
||||||
self.set_selection(
|
|
||||||
new_pos,
|
|
||||||
selection.end_pos
|
|
||||||
)
|
|
||||||
} else if new_pos < caret_pos {
|
|
||||||
self.set_selection(
|
|
||||||
selection.start_pos,
|
|
||||||
new_pos
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if new_pos < self.caret_pos {
|
|
||||||
self.set_selection(
|
|
||||||
new_pos,
|
|
||||||
caret_pos
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
self.set_selection(
|
|
||||||
caret_pos,
|
|
||||||
new_pos
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.selection_opt = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.caret_pos = new_pos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
15
editor/src/ui/util.rs
Normal file
15
editor/src/ui/util.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
use super::ui_error::{UIResult, OutOfBounds};
|
||||||
|
use snafu::OptionExt;
|
||||||
|
use std::slice::SliceIndex;
|
||||||
|
|
||||||
|
// replace vec methods that return Option with ones that return Result and proper Error
|
||||||
|
pub fn slice_get<T>(index: usize, slice: &[T]) -> UIResult<&<usize as SliceIndex<[T]>>::Output> {
|
||||||
|
let elt_ref = slice.get(index).context(OutOfBounds {
|
||||||
|
index,
|
||||||
|
collection_name: "Slice",
|
||||||
|
len: slice.len(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(elt_ref)
|
||||||
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
pub fn is_newline(char_ref: &char) -> bool {
|
|
||||||
let newline_codes = vec!['\u{d}', '\n'];
|
|
||||||
|
|
||||||
newline_codes.contains(char_ref)
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue