TEXT SELECTION
This commit is contained in:
@@ -6,7 +6,12 @@ use iris::prelude::*;
|
||||
use len_fns::*;
|
||||
use render::Renderer;
|
||||
use std::sync::Arc;
|
||||
use winit::{event::WindowEvent, event_loop::ActiveEventLoop, window::Window};
|
||||
use winit::{
|
||||
dpi::{LogicalPosition, LogicalSize},
|
||||
event::{Ime, WindowEvent},
|
||||
event_loop::ActiveEventLoop,
|
||||
window::Window,
|
||||
};
|
||||
|
||||
mod app;
|
||||
mod input;
|
||||
@@ -18,11 +23,13 @@ fn main() {
|
||||
|
||||
pub struct Client {
|
||||
renderer: Renderer,
|
||||
window: Arc<Window>,
|
||||
input: Input,
|
||||
ui: Ui,
|
||||
info: WidgetId<Text>,
|
||||
focus: Option<WidgetId<TextEdit>>,
|
||||
clipboard: Clipboard,
|
||||
ime: usize,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Clone)]
|
||||
@@ -32,9 +39,37 @@ impl DefaultEvent for Submit {
|
||||
type Data = ();
|
||||
}
|
||||
|
||||
pub struct Selectable;
|
||||
|
||||
impl WidgetAttr<TextEdit> for Selectable {
|
||||
type Input = ();
|
||||
|
||||
fn run(ui: &mut Ui, id: &WidgetId<TextEdit>, _: Self::Input) {
|
||||
let id = id.clone();
|
||||
ui.register_event(
|
||||
&id.clone(),
|
||||
CursorSense::click_or_drag(),
|
||||
move |client: &mut Client, data| {
|
||||
client
|
||||
.ui
|
||||
.text(&id)
|
||||
.select(data.cursor, data.size, data.sense.is_dragging());
|
||||
if let Some(region) = client.ui.window_region(&id) {
|
||||
client.window.set_ime_allowed(true);
|
||||
client.window.set_ime_cursor_area(
|
||||
LogicalPosition::<f32>::from(region.top_left.tuple()),
|
||||
LogicalSize::<f32>::from(region.size().tuple()),
|
||||
);
|
||||
}
|
||||
client.focus = Some(id.clone());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(window: Arc<Window>) -> Self {
|
||||
let renderer = Renderer::new(window);
|
||||
let renderer = Renderer::new(window.clone());
|
||||
|
||||
let mut ui = Ui::new();
|
||||
let rrect = rect(Color::WHITE).radius(20);
|
||||
@@ -127,10 +162,7 @@ impl Client {
|
||||
.editable()
|
||||
.text_align(Align::LEFT)
|
||||
.size(30)
|
||||
.id_on(CursorSense::click(), |id, client: &mut Client, ctx| {
|
||||
client.ui.text(id).select(ctx.cursor, ctx.size);
|
||||
client.focus = Some(id.clone());
|
||||
})
|
||||
.attr::<Selectable>(())
|
||||
.id_on(Submit, move |id, client: &mut Client, _| {
|
||||
let content = client.ui.text(id).take();
|
||||
let text = text(content)
|
||||
@@ -138,10 +170,7 @@ impl Client {
|
||||
.size(30)
|
||||
.text_align(Align::LEFT)
|
||||
.wrap(true)
|
||||
.id_on(CursorSense::click(), |id, client: &mut Client, ctx| {
|
||||
client.ui.text(id).select(ctx.cursor, ctx.size);
|
||||
client.focus = Some(id.clone());
|
||||
});
|
||||
.attr::<Selectable>(());
|
||||
let msg_box = text
|
||||
.background(rect(Color::WHITE.darker(0.5)))
|
||||
.add(&mut client.ui);
|
||||
@@ -213,11 +242,13 @@ impl Client {
|
||||
|
||||
Self {
|
||||
renderer,
|
||||
window,
|
||||
input: Input::default(),
|
||||
ui,
|
||||
info,
|
||||
focus: None,
|
||||
clipboard: Clipboard::new().unwrap(),
|
||||
ime: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,10 +294,31 @@ impl Client {
|
||||
text.insert(&t);
|
||||
}
|
||||
}
|
||||
TextInputResult::Copy(text) => {
|
||||
if let Err(err) = self.clipboard.set_text(text) {
|
||||
eprintln!("failed to copy text to clipboard: {err}")
|
||||
}
|
||||
}
|
||||
TextInputResult::Unused | TextInputResult::Used => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowEvent::Ime(ime) => {
|
||||
if let Some(sel) = &self.focus {
|
||||
let mut text = self.ui.text(sel);
|
||||
match ime {
|
||||
Ime::Enabled | Ime::Disabled => (),
|
||||
Ime::Preedit(content, _pos) => {
|
||||
// TODO: highlight once that's real
|
||||
text.replace(self.ime, &content);
|
||||
self.ime = content.chars().count();
|
||||
}
|
||||
Ime::Commit(content) => {
|
||||
text.insert(&content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
let new = format!(
|
||||
|
||||
@@ -10,14 +10,14 @@ use crate::{
|
||||
util::{HashMap, Id},
|
||||
};
|
||||
|
||||
#[derive(PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum Button {
|
||||
Left,
|
||||
Right,
|
||||
Middle,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum CursorSense {
|
||||
PressStart(Button),
|
||||
Pressing(Button),
|
||||
@@ -34,9 +34,15 @@ impl CursorSense {
|
||||
pub fn click() -> Self {
|
||||
Self::PressStart(Button::Left)
|
||||
}
|
||||
pub fn click_or_drag() -> CursorSenses {
|
||||
Self::click() | Self::Pressing(Button::Left)
|
||||
}
|
||||
pub fn unclick() -> Self {
|
||||
Self::PressEnd(Button::Left)
|
||||
}
|
||||
pub fn is_dragging(&self) -> bool {
|
||||
matches!(self, CursorSense::Pressing(Button::Left))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
@@ -108,6 +114,9 @@ pub struct CursorData {
|
||||
pub cursor: Vec2,
|
||||
pub size: Vec2,
|
||||
pub scroll_delta: Vec2,
|
||||
/// the (first) sense that triggered this event
|
||||
/// the senses are checked in order
|
||||
pub sense: CursorSense,
|
||||
}
|
||||
|
||||
pub struct CursorModule<Ctx> {
|
||||
@@ -188,11 +197,12 @@ impl<Ctx: UiCtx + 'static> CursorModule<Ctx> {
|
||||
sensed = true;
|
||||
|
||||
for sensor in &mut group.sensors {
|
||||
if should_run(&sensor.senses, cursor, group.hover) {
|
||||
if let Some(sense) = should_run(&sensor.senses, cursor, group.hover) {
|
||||
let data = CursorData {
|
||||
cursor: cursor.pos - region.top_left,
|
||||
size: region.bot_right - region.top_left,
|
||||
scroll_delta: cursor.scroll_delta,
|
||||
sense,
|
||||
};
|
||||
(sensor.f)(ctx, data);
|
||||
}
|
||||
@@ -210,7 +220,11 @@ impl<Ctx: UiCtx + 'static> CursorModule<Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_run(senses: &CursorSenses, cursor: &CursorState, hover: ActivationState) -> bool {
|
||||
pub fn should_run(
|
||||
senses: &CursorSenses,
|
||||
cursor: &CursorState,
|
||||
hover: ActivationState,
|
||||
) -> Option<CursorSense> {
|
||||
for sense in senses.iter() {
|
||||
if match sense {
|
||||
CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(),
|
||||
@@ -221,10 +235,10 @@ pub fn should_run(senses: &CursorSenses, cursor: &CursorState, hover: Activation
|
||||
CursorSense::HoverEnd => hover.is_end(),
|
||||
CursorSense::Scroll => cursor.scroll_delta != Vec2::ZERO,
|
||||
} {
|
||||
return true;
|
||||
return Some(*sense);
|
||||
}
|
||||
}
|
||||
false
|
||||
None
|
||||
}
|
||||
|
||||
impl ActivationState {
|
||||
@@ -280,8 +294,8 @@ impl Event for CursorSense {
|
||||
type Data = CursorData;
|
||||
}
|
||||
|
||||
impl<E: Event<Data = <CursorSenses as Event>::Data> + Into<CursorSenses>, Ctx: 'static> EventModule<E, Ctx>
|
||||
for CursorModule<Ctx>
|
||||
impl<E: Event<Data = <CursorSenses as Event>::Data> + Into<CursorSenses>, Ctx: 'static>
|
||||
EventModule<E, Ctx> for CursorModule<Ctx>
|
||||
{
|
||||
fn register(&mut self, id: Id, senses: E, f: impl EventFn<Ctx, <E as Event>::Data>) {
|
||||
// TODO: does not add to active if currently active
|
||||
|
||||
@@ -94,7 +94,7 @@ impl TextBuilderOutput for TextEditOutput {
|
||||
));
|
||||
let mut text = TextEdit {
|
||||
view: TextView::new(buf, builder.attrs, builder.hint.get(ui)),
|
||||
cursor: None,
|
||||
selection: Default::default(),
|
||||
};
|
||||
let font_system = &mut ui.data.text.font_system;
|
||||
text.buf
|
||||
|
||||
@@ -10,7 +10,7 @@ use winit::{
|
||||
|
||||
pub struct TextEdit {
|
||||
pub(super) view: TextView,
|
||||
pub(super) cursor: Option<Cursor>,
|
||||
pub(super) selection: TextSelection,
|
||||
}
|
||||
|
||||
impl TextEdit {
|
||||
@@ -21,28 +21,86 @@ impl TextEdit {
|
||||
.align(self.align)
|
||||
}
|
||||
|
||||
pub fn content(&self) -> String {
|
||||
self.buf
|
||||
.lines
|
||||
.iter()
|
||||
.map(|l| l.text())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
pub fn select_content(&self, start: Cursor, end: Cursor) -> String {
|
||||
let (start, end) = sort_cursors(start, end);
|
||||
let mut iter = self.buf.lines.iter().skip(start.line);
|
||||
let first = iter.next().unwrap();
|
||||
if start.line == end.line {
|
||||
first.text()[start.index..end.index].to_string()
|
||||
} else {
|
||||
let mut str = first.text()[start.index..].to_string();
|
||||
for _ in (start.line + 1)..end.line {
|
||||
str = str + "\n" + iter.next().unwrap().text();
|
||||
}
|
||||
let last = iter.next().unwrap();
|
||||
str = str + "\n" + &last.text()[..end.index];
|
||||
str
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for TextEdit {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
let base = painter.layer;
|
||||
painter.child_layer();
|
||||
let region = self.view.draw(painter);
|
||||
painter.layer = base;
|
||||
|
||||
if let Some(cursor) = &self.cursor
|
||||
&& let Some(offset) = cursor_pos(cursor, &self.buf)
|
||||
{
|
||||
let size = vec2(1, self.attrs.line_height);
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(Color::WHITE),
|
||||
size.align(Align::TOP_LEFT).offset(offset).within(®ion),
|
||||
);
|
||||
match &self.selection {
|
||||
TextSelection::None => (),
|
||||
TextSelection::Pos(cursor) => {
|
||||
if let Some(offset) = cursor_pos(cursor, &self.buf) {
|
||||
let size = vec2(1, self.attrs.line_height);
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(Color::WHITE),
|
||||
size.align(Align::TOP_LEFT).offset(offset).within(®ion),
|
||||
);
|
||||
}
|
||||
}
|
||||
TextSelection::Span { start, end } => {
|
||||
let (start, end) = sort_cursors(*start, *end);
|
||||
let line_height = self.attrs.line_height;
|
||||
let mut highlight = |offset, width: f32| {
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(Color::SKY),
|
||||
vec2(width, line_height)
|
||||
.align(Align::TOP_LEFT)
|
||||
.offset(offset)
|
||||
.within(®ion),
|
||||
);
|
||||
};
|
||||
if let (Some(start_offset), Some(end_offset)) =
|
||||
(cursor_pos(&start, &self.buf), cursor_pos(&end, &self.buf))
|
||||
{
|
||||
let mut iter = self.buf.layout_runs().skip(start.line);
|
||||
if start.line == end.line {
|
||||
highlight(start_offset, end_offset.x - start_offset.x);
|
||||
} else {
|
||||
let first = iter.next().unwrap();
|
||||
highlight(start_offset, first.line_w - start_offset.x);
|
||||
for i in (start.line + 1)..end.line {
|
||||
let line = iter.next().unwrap();
|
||||
let offset = vec2(0.0, i as f32 * line_height);
|
||||
highlight(offset, line.line_w);
|
||||
}
|
||||
let offset = vec2(0.0, end.line as f32 * line_height);
|
||||
highlight(offset, end_offset.x);
|
||||
}
|
||||
// painter.primitive_within(
|
||||
// RectPrimitive::color(Color::WHITE),
|
||||
// size.align(Align::TOP_LEFT)
|
||||
// .offset(end_offset)
|
||||
// .within(®ion),
|
||||
// );
|
||||
let size = vec2(1, self.attrs.line_height);
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(Color::WHITE),
|
||||
size.align(Align::TOP_LEFT)
|
||||
.offset(end_offset)
|
||||
.within(®ion),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,17 +114,17 @@ impl Widget for TextEdit {
|
||||
}
|
||||
|
||||
/// copied & modified from fn found in Editor in cosmic_text
|
||||
fn cursor_pos(cursor: &Cursor, buf: &TextBuffer) -> Option<(f32, f32)> {
|
||||
fn cursor_pos(cursor: &Cursor, buf: &TextBuffer) -> Option<Vec2> {
|
||||
let mut prev = None;
|
||||
for run in buf
|
||||
.layout_runs()
|
||||
.skip_while(|r| r.line_i < cursor.line)
|
||||
.take_while(|r| r.line_i == cursor.line)
|
||||
{
|
||||
prev = Some((run.line_w, run.line_top));
|
||||
prev = Some(vec2(run.line_w, run.line_top));
|
||||
for glyph in run.glyphs.iter() {
|
||||
if cursor.index == glyph.start {
|
||||
return Some((glyph.x, run.line_top));
|
||||
return Some(vec2(glyph.x, run.line_top));
|
||||
} else if cursor.index > glyph.start && cursor.index < glyph.end {
|
||||
// Guess x offset based on characters
|
||||
let mut before = 0;
|
||||
@@ -81,7 +139,7 @@ fn cursor_pos(cursor: &Cursor, buf: &TextBuffer) -> Option<(f32, f32)> {
|
||||
}
|
||||
|
||||
let offset = glyph.w * (before as f32) / (total as f32);
|
||||
return Some((glyph.x + offset, run.line_top));
|
||||
return Some(vec2(glyph.x + offset, run.line_top));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,22 +164,18 @@ impl<'a> TextEditCtx<'a> {
|
||||
self.text
|
||||
.buf
|
||||
.set_text(self.font_system, "", &Attrs::new(), SHAPING, None);
|
||||
if let Some(cursor) = &mut self.text.cursor {
|
||||
cursor.line = 0;
|
||||
cursor.index = 0;
|
||||
cursor.affinity = Affinity::default();
|
||||
}
|
||||
self.text.selection.clear();
|
||||
text
|
||||
}
|
||||
|
||||
pub fn motion(&mut self, motion: Motion) {
|
||||
if let Some(cursor) = self.text.cursor
|
||||
if let TextSelection::Pos(cursor) = self.text.selection
|
||||
&& let Some((cursor, _)) =
|
||||
self.text
|
||||
.buf
|
||||
.cursor_motion(self.font_system, cursor, None, motion)
|
||||
{
|
||||
self.text.cursor = Some(cursor);
|
||||
self.text.selection = TextSelection::Pos(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,12 +198,41 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_span(&mut self) -> bool {
|
||||
if let TextSelection::Span { start, end } = self.text.selection {
|
||||
let lines = &mut self.text.view.buf.lines;
|
||||
let (start, end) = sort_cursors(start, end);
|
||||
if start.line == end.line {
|
||||
let line = &mut lines[start.line];
|
||||
let text = line.text();
|
||||
let text = text[..start.index].to_string() + &text[end.index..];
|
||||
edit_line(line, text);
|
||||
} else {
|
||||
// start
|
||||
let start_text = lines[start.line].text()[..start.index].to_string();
|
||||
let end_text = &lines[end.line].text()[end.index..];
|
||||
let text = start_text + end_text;
|
||||
edit_line(&mut lines[start.line], text);
|
||||
}
|
||||
// between
|
||||
let range = (start.line + 1)..=end.line;
|
||||
if !range.is_empty() {
|
||||
lines.splice(range, None);
|
||||
}
|
||||
self.text.selection = TextSelection::Pos(start);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_inner(&mut self, text: &str, mov: bool) {
|
||||
if let Some(cursor) = &mut self.text.cursor {
|
||||
self.clear_span();
|
||||
if let TextSelection::Pos(cursor) = &mut self.text.selection {
|
||||
let line = &mut self.text.view.buf.lines[cursor.line];
|
||||
let mut line_text = line.text().to_string();
|
||||
line_text.insert_str(cursor.index, text);
|
||||
line.set_text(line_text, line.ending(), line.attrs_list().clone());
|
||||
edit_line(line, line_text);
|
||||
if mov {
|
||||
for _ in 0..text.chars().count() {
|
||||
self.motion(Motion::Right);
|
||||
@@ -159,7 +242,8 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
|
||||
pub fn newline(&mut self) {
|
||||
if let Some(cursor) = &mut self.text.cursor {
|
||||
self.clear_span();
|
||||
if let TextSelection::Pos(cursor) = &mut self.text.selection {
|
||||
let lines = &mut self.text.view.buf.lines;
|
||||
let line = &mut lines[cursor.line];
|
||||
let new = line.split_off(cursor.index);
|
||||
@@ -170,7 +254,8 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self) {
|
||||
if let Some(cursor) = &mut self.text.cursor
|
||||
if !self.clear_span()
|
||||
&& let TextSelection::Pos(cursor) = &mut self.text.selection
|
||||
&& (cursor.index != 0 || cursor.line != 0)
|
||||
{
|
||||
self.motion(Motion::Left);
|
||||
@@ -179,7 +264,9 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
|
||||
pub fn delete(&mut self) {
|
||||
if let Some(cursor) = &mut self.text.cursor {
|
||||
if !self.clear_span()
|
||||
&& let TextSelection::Pos(cursor) = &mut self.text.selection
|
||||
{
|
||||
let lines = &mut self.text.view.buf.lines;
|
||||
let line = &mut lines[cursor.line];
|
||||
if cursor.index == line.text().len() {
|
||||
@@ -190,22 +277,47 @@ impl<'a> TextEditCtx<'a> {
|
||||
let line = &mut lines[cursor.line];
|
||||
let mut cur = line.text().to_string();
|
||||
cur.push_str(&add);
|
||||
line.set_text(cur, line.ending(), line.attrs_list().clone());
|
||||
edit_line(line, cur);
|
||||
} else {
|
||||
let mut text = line.text().to_string();
|
||||
text.remove(cursor.index);
|
||||
line.set_text(text, line.ending(), line.attrs_list().clone());
|
||||
edit_line(line, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select(&mut self, pos: Vec2, size: Vec2) {
|
||||
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool) {
|
||||
let pos = pos - self.text.region().top_left().to_abs(size);
|
||||
self.text.cursor = self.text.buf.hit(pos.x, pos.y);
|
||||
let hit = self.text.buf.hit(pos.x, pos.y);
|
||||
let sel = &mut self.text.selection;
|
||||
match sel {
|
||||
TextSelection::None => {
|
||||
if !drag && let Some(hit) = hit {
|
||||
*sel = TextSelection::Pos(hit)
|
||||
}
|
||||
}
|
||||
TextSelection::Pos(pos) => match (hit, drag) {
|
||||
(None, false) => *sel = TextSelection::None,
|
||||
(None, true) => (),
|
||||
(Some(hit), false) => *pos = hit,
|
||||
(Some(end), true) => *sel = TextSelection::Span { start: *pos, end },
|
||||
},
|
||||
TextSelection::Span { start, end } => match (hit, drag) {
|
||||
(None, false) => *sel = TextSelection::None,
|
||||
(None, true) => *sel = TextSelection::Pos(*start),
|
||||
(Some(hit), false) => *sel = TextSelection::Pos(hit),
|
||||
(Some(hit), true) => *end = hit,
|
||||
},
|
||||
}
|
||||
if let TextSelection::Span { start, end } = sel
|
||||
&& start == end
|
||||
{
|
||||
*sel = TextSelection::Pos(*start);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deselect(&mut self) {
|
||||
self.text.cursor = None;
|
||||
self.text.selection = TextSelection::None;
|
||||
}
|
||||
|
||||
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
|
||||
@@ -232,11 +344,20 @@ impl<'a> TextEditCtx<'a> {
|
||||
_ => return TextInputResult::Unused,
|
||||
},
|
||||
Key::Character(text) => {
|
||||
if modifiers.control && text == "v" {
|
||||
return TextInputResult::Paste;
|
||||
} else {
|
||||
self.insert(text)
|
||||
if modifiers.control {
|
||||
match text.as_str() {
|
||||
"v" => return TextInputResult::Paste,
|
||||
"c" => {
|
||||
if let TextSelection::Span { start, end } = self.text.selection {
|
||||
let content = self.text.select_content(start, end);
|
||||
return TextInputResult::Copy(content);
|
||||
}
|
||||
return TextInputResult::Used;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
self.insert(text)
|
||||
}
|
||||
_ => return TextInputResult::Unused,
|
||||
}
|
||||
@@ -262,9 +383,37 @@ pub enum TextInputResult {
|
||||
Unused,
|
||||
Unfocus,
|
||||
Submit,
|
||||
Copy(String),
|
||||
Paste,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum TextSelection {
|
||||
#[default]
|
||||
None,
|
||||
Pos(Cursor),
|
||||
Span {
|
||||
start: Cursor,
|
||||
end: Cursor,
|
||||
},
|
||||
}
|
||||
|
||||
impl TextSelection {
|
||||
pub fn clear(&mut self) {
|
||||
match self {
|
||||
TextSelection::None => (),
|
||||
TextSelection::Pos(cursor) => {
|
||||
cursor.line = 0;
|
||||
cursor.index = 0;
|
||||
cursor.affinity = Affinity::default();
|
||||
}
|
||||
TextSelection::Span { start: _, end: _ } => {
|
||||
*self = TextSelection::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextInputResult {
|
||||
pub fn unfocus(&self) -> bool {
|
||||
matches!(self, TextInputResult::Unfocus)
|
||||
|
||||
@@ -5,7 +5,7 @@ pub use build::*;
|
||||
pub use edit::*;
|
||||
|
||||
use crate::{prelude::*, util::MutDetect};
|
||||
use cosmic_text::{Attrs, Metrics, Shaping};
|
||||
use cosmic_text::{Attrs, BufferLine, Cursor, Metrics, Shaping};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
pub const SHAPING: Shaping = Shaping::Advanced;
|
||||
@@ -82,6 +82,15 @@ impl TextView {
|
||||
painter.texture_within(&tex.handle, region);
|
||||
region
|
||||
}
|
||||
|
||||
pub fn content(&self) -> String {
|
||||
self.buf
|
||||
.lines
|
||||
.iter()
|
||||
.map(|l| l.text())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl Text {
|
||||
@@ -124,6 +133,16 @@ impl Widget for Text {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sort_cursors(a: Cursor, b: Cursor) -> (Cursor, Cursor) {
|
||||
let start = a.min(b);
|
||||
let end = a.max(b);
|
||||
(start, end)
|
||||
}
|
||||
|
||||
pub fn edit_line(line: &mut BufferLine, text: String) {
|
||||
line.set_text(text, line.ending(), line.attrs_list().clone());
|
||||
}
|
||||
|
||||
impl Deref for Text {
|
||||
type Target = TextAttrs;
|
||||
|
||||
|
||||
20
src/layout/attr.rs
Normal file
20
src/layout/attr.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use crate::layout::{Ui, WidgetId, WidgetIdFn, WidgetLike};
|
||||
|
||||
pub trait WidgetAttr<W> {
|
||||
type Input;
|
||||
fn run(ui: &mut Ui, id: &WidgetId<W>, input: Self::Input);
|
||||
}
|
||||
|
||||
pub trait Attrable<W, Tag> {
|
||||
fn attr<A: WidgetAttr<W>>(self, input: A::Input) -> impl WidgetIdFn<W>;
|
||||
}
|
||||
|
||||
impl<WL: WidgetLike<Tag>, Tag> Attrable<WL::Widget, Tag> for WL {
|
||||
fn attr<A: WidgetAttr<WL::Widget>>(self, input: A::Input) -> impl WidgetIdFn<WL::Widget> {
|
||||
|ui| {
|
||||
let id = self.add(ui);
|
||||
A::run(ui, &id, input);
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,10 +55,7 @@ impl<W: WidgetLike<Tag>, Tag> Eventable<W::Widget, Tag> for W {
|
||||
) -> impl WidgetIdFn<W::Widget> {
|
||||
move |ui| {
|
||||
let id = self.add(ui);
|
||||
ui.data
|
||||
.modules
|
||||
.get_mut::<E::Module<Ctx>>()
|
||||
.register(id.id, event, f);
|
||||
ui.register_event(&id, event, f);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ mod painter;
|
||||
mod text;
|
||||
mod texture;
|
||||
mod ui;
|
||||
mod attr;
|
||||
mod vec2;
|
||||
mod widget;
|
||||
mod widgets;
|
||||
@@ -24,6 +25,7 @@ pub use painter::*;
|
||||
pub use text::*;
|
||||
pub use texture::*;
|
||||
pub use ui::*;
|
||||
pub use attr::*;
|
||||
pub use vec2::*;
|
||||
pub use widget::*;
|
||||
pub use widgets::*;
|
||||
|
||||
@@ -17,8 +17,6 @@ pub struct Painter<'a, 'c> {
|
||||
children: Vec<Id>,
|
||||
children_width: HashMap<Id, (UiVec2, Len)>,
|
||||
children_height: HashMap<Id, (UiVec2, Len)>,
|
||||
/// whether this widget depends on region's final pixel size or not
|
||||
/// TODO: decide if point (pt) should be used here instead of px
|
||||
pub layer: usize,
|
||||
id: Id,
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ use image::DynamicImage;
|
||||
use crate::{
|
||||
core::{TextEdit, TextEditCtx},
|
||||
layout::{
|
||||
IdLike, PainterData, PixelRegion, StaticWidgetId, TextureHandle, Vec2, Widget,
|
||||
WidgetId, WidgetInstance, WidgetLike,
|
||||
Event, EventFn, EventModule, IdLike, PainterData, PixelRegion, StaticWidgetId,
|
||||
TextureHandle, Vec2, Widget, WidgetId, WidgetInstance, WidgetLike,
|
||||
},
|
||||
util::{HashSet, Id},
|
||||
};
|
||||
@@ -96,6 +96,18 @@ impl Ui {
|
||||
self.data.textures.add(image)
|
||||
}
|
||||
|
||||
pub fn register_event<W, E: Event, Ctx: 'static>(
|
||||
&mut self,
|
||||
id: &WidgetId<W>,
|
||||
event: E,
|
||||
f: impl EventFn<Ctx, E::Data>,
|
||||
) {
|
||||
self.data
|
||||
.modules
|
||||
.get_mut::<E::Module<Ctx>>()
|
||||
.register(id.id, event, f);
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||
self.data.output_size = size.into();
|
||||
self.resized = true;
|
||||
|
||||
Reference in New Issue
Block a user