-- text editor, particularly text drawing, horizontal wrap, vertical scrolling Text = {} -- draw a line starting from startpos to screen at y between State.left and State.right -- return y for the next line, and position of start of final screen line drawn function Text.draw(State, line_index, y, startpos) --? print('text.draw', line_index, y) local line = State.lines[line_index] local line_cache = State.line_cache[line_index] line_cache.starty = y line_cache.startpos = startpos -- wrap long lines local final_screen_line_starting_pos = startpos -- track value to return Text.populate_screen_line_starting_pos(State, line_index) assert(#line_cache.screen_line_starting_pos >= 1, 'line cache missing screen line info') for i=1,#line_cache.screen_line_starting_pos do local pos = line_cache.screen_line_starting_pos[i] if pos < startpos then -- render nothing else final_screen_line_starting_pos = pos local screen_line = Text.screen_line(line, line_cache, i) --? print('text.draw:', screen_line, 'at', line_index,pos, 'after', x,y) local frag_len = utf8.len(screen_line) -- render any highlights if State.selection1.line then local lo, hi = Text.clip_selection(State, line_index, pos, pos+frag_len) Text.draw_highlight(State, line, State.left,y, pos, lo,hi) end if line_index == State.cursor1.line then -- render search highlight or cursor if State.search_term then local data = State.lines[State.cursor1.line].data local cursor_offset = Text.offset(data, State.cursor1.pos) if data:sub(cursor_offset, cursor_offset+#State.search_term-1) == State.search_term then local save_selection = State.selection1 State.selection1 = {line=line_index, pos=State.cursor1.pos+utf8.len(State.search_term)} local lo, hi = Text.clip_selection(State, line_index, pos, pos+frag_len) Text.draw_highlight(State, line, State.left,y, pos, lo,hi) State.selection1 = save_selection end else if pos <= State.cursor1.pos and pos + frag_len > State.cursor1.pos then Text.draw_cursor(State, State.left+Text.x(screen_line, State.cursor1.pos-pos+1), y) elseif pos + frag_len == State.cursor1.pos then -- Show cursor at end of line. -- This place also catches end of wrapping screen lines. That doesn't seem worth distinguishing. -- It seems useful to see a cursor whether your eye is on the left or right margin. Text.draw_cursor(State, State.left+Text.x(screen_line, State.cursor1.pos-pos+1), y) end end end -- render fragment App.color(Text_color) App.screen.print(screen_line, State.left,y) y = y + State.line_height if y >= App.screen.height then break end end end return y, final_screen_line_starting_pos end function Text.screen_line(line, line_cache, i) local pos = line_cache.screen_line_starting_pos[i] local offset = Text.offset(line.data, pos) if i >= #line_cache.screen_line_starting_pos then return line.data:sub(offset) end local endpos = line_cache.screen_line_starting_pos[i+1]-1 local end_offset = Text.offset(line.data, endpos) return line.data:sub(offset, end_offset) end function Text.draw_cursor(State, x, y) -- blink every 0.5s if math.floor(Cursor_time*2)%2 == 0 then App.color(Cursor_color) love.graphics.rectangle('fill', x,y, 3,State.line_height) end State.cursor_x = x State.cursor_y = y+State.line_height end function Text.populate_screen_line_starting_pos(State, line_index) local line = State.lines[line_index] if line.mode ~= 'text' then return end local line_cache = State.line_cache[line_index] if line_cache.screen_line_starting_pos then return end line_cache.screen_line_starting_pos = {1} local x = 0 local pos = 1 -- try to wrap at word boundaries for frag in line.data:gmatch('%S*%s*') do local frag_width = App.width(frag) --? print('-- frag:', frag, pos, x, frag_width, State.width) while x + frag_width > State.width do --? print('frag:', frag, pos, x, frag_width, State.width) if x < 0.8 * State.width then -- long word; chop it at some letter -- We're not going to reimplement TeX here. local bpos = Text.nearest_pos_less_than(frag, State.width - x) if x == 0 and bpos == 0 then assert(false, ("Infinite loop while line-wrapping. Editor is %dpx wide; window is %dpx wide"):format(State.width, App.screen.width)) end pos = pos + bpos local boffset = Text.offset(frag, bpos+1) -- byte _after_ bpos frag = string.sub(frag, boffset) --? if bpos > 0 then --? print('after chop:', frag) --? end frag_width = App.width(frag) end --? print('screen line:', pos) table.insert(line_cache.screen_line_starting_pos, pos) x = 0 -- new screen line end x = x + frag_width pos = pos + utf8.len(frag) end end function Text.text_input(State, t) if App.mouse_down(1) then return end if App.any_modifier_down() then if App.key_down(t) then -- The modifiers didn't change the key. Handle it in keychord_pressed. return else -- Key mutated by the keyboard layout. Continue below. end end local before = snapshot(State, State.cursor1.line) --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) Text.insert_at_cursor(State, t) if State.cursor_y > App.screen.height - State.line_height then Text.populate_screen_line_starting_pos(State, State.cursor1.line) Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right) end record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) end function Text.insert_at_cursor(State, t) assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text') local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset) Text.clear_screen_line_cache(State, State.cursor1.line) State.cursor1.pos = State.cursor1.pos+1 end -- Don't handle any keys here that would trigger text_input above. function Text.keychord_press(State, chord) --? print('chord', chord, State.selection1.line, State.selection1.pos) --== shortcuts that mutate text if chord == 'return' then local before_line = State.cursor1.line local before = snapshot(State, before_line) Text.insert_return(State) State.selection1 = {} if State.cursor_y > App.screen.height - State.line_height then Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right) end schedule_save(State) record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)}) elseif chord == 'tab' then local before = snapshot(State, State.cursor1.line) --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) Text.insert_at_cursor(State, '\t') if State.cursor_y > App.screen.height - State.line_height then Text.populate_screen_line_starting_pos(State, State.cursor1.line) Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right) --? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) end schedule_save(State) record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) elseif chord == 'backspace' then if State.selection1.line then Text.delete_selection(State, State.left, State.right) schedule_save(State) return end local before if State.cursor1.pos > 1 then before = snapshot(State, State.cursor1.line) local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos-1) local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) if byte_start then if byte_end then State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end) else State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1) end State.cursor1.pos = State.cursor1.pos-1 end elseif State.cursor1.line > 1 then before = snapshot(State, State.cursor1.line-1, State.cursor1.line) if State.lines[State.cursor1.line-1].mode == 'drawing' then table.remove(State.lines, State.cursor1.line-1) table.remove(State.line_cache, State.cursor1.line-1) else -- join lines State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1 State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data table.remove(State.lines, State.cursor1.line) table.remove(State.line_cache, State.cursor1.line) end State.cursor1.line = State.cursor1.line-1 end if State.screen_top1.line > #State.lines then Text.populate_screen_line_starting_pos(State, #State.lines) local line_cache = State.line_cache[#State.line_cache] State.screen_top1 = {line=#State.lines, pos=line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]} elseif Text.lt1(State.cursor1, State.screen_top1) then State.screen_top1 = { line=State.cursor1.line, pos=Text.pos_at_start_of_screen_line(State, State.cursor1), } Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks end Text.clear_screen_line_cache(State, State.cursor1.line) assert(Text.le1(State.screen_top1, State.cursor1), ('screen_top (line=%d,pos=%d) is below cursor (line=%d,pos=%d)'):format(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)) schedule_save(State) record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) elseif chord == 'delete' then if State.selection1.line then Text.delete_selection(State, State.left, State.right) schedule_save(State) return end local before if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then before = snapshot(State, State.cursor1.line) else before = snapshot(State, State.cursor1.line, State.cursor1.line+1) end if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos+1) if byte_start then if byte_end then State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end) else State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1) end -- no change to State.cursor1.pos end elseif State.cursor1.line < #State.lines then if State.lines[State.cursor1.line+1].mode == 'text' then -- join lines State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data end table.remove(State.lines, State.cursor1.line+1) table.remove(State.line_cache, State.cursor1.line+1) end Text.clear_screen_line_cache(State, State.cursor1.line) schedule_save(State) record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) --== shortcuts that move the cursor elseif chord == 'left' then Text.left(State) State.selection1 = {} elseif chord == 'right' then Text.right(State) State.selection1 = {} elseif chord == 'S-left' then if State.selection1.line == nil then State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} end Text.left(State) elseif chord == 'S-right' then if State.selection1.line == nil then State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} end Text.right(State) -- C- hotkeys reserved for drawings, so we'll use M- elseif chord == 'M-left' then Text.word_left(State) State.selection1 = {} elseif chord == 'M-right' then Text.word_right(State) State.selection1 = {} elseif chord == 'M-S-left' then if State.selection1.line == nil then State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} end Text.word_left(State) elseif chord == 'M-S-right' then if State.selection1.line == nil then State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} end Text.word_right(State) elseif chord == 'home' then Text.start_of_line(State) State.selection1 = {} elseif chord == 'end' then
//: Clean syntax to manipulate and check the console in scenarios.
//: Instruction 'assume-console' implicitly creates a variable called
//: 'console' that is accessible inside other 'run' instructions in the
//: scenario. Like with the fake screen, 'assume-console' transparently
//: supports unicode.
//: first make sure we don't mangle this instruction in other transforms
:(before "End initialize_transform_rewrite_literal_string_to_text()")
recipes_taking_literal_strings.insert("assume-console");
:(scenarios run_mu_scenario)
:(scenario keyboard_in_scenario)
scenario keyboard-in-scenario [
assume-console [
type [abc]
]
run [
1:char, 2:bool <- read-key console
3:char, 4:bool <- read-key console
5:char, 6:bool <- read-key console
7:char, 8:bool, 9:bool <- read-key console
]
memory-should-contain [
1 <- 97 # 'a'
2 <- 1
3 <- 98 # 'b'
4 <- 1
5 <- 99 # 'c'
6 <- 1
7 <- 0 # unset
8 <- 1
9 <- 1 # end of test events
]
]
:(before "End Scenario Globals")
extern const int CONSOLE = next_predefined_global_for_scenarios(/*size_of(address:console)*/2);
//: give 'console' a fixed location in scenarios
:(before "End Special Scenario Variable Names(r)")
Name[r]["console"] = CONSOLE;
//: make 'console' always a raw location in scenarios
:(before "End is_special_name Special-cases")
if (s == "console") return true;
:(before "End Primitive Recipe Declarations")
ASSUME_CONSOLE,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "assume-console", ASSUME_CONSOLE);
:(before "End Primitive Recipe Checks")
case ASSUME_CONSOLE: {
break;
}
:(before "End Primitive Recipe Implementations")
case ASSUME_CONSOLE: {
// create a temporary recipe just for parsing; it won't contain valid instructions
istringstream in("[" + current_instruction().ingredients.at(0).name + "]");
recipe r;
slurp_body(in, r);
int num_events = count_events(r);
// initialize the events like in new-fake-console
int size = /*length*/1 + num_events*size_of_event();
int event_data_address = allocate(size);
// store length
put(Memory, event_data_address+/*skip alloc id*/1, num_events);
int curr_address = event_data_address + /*skip alloc id*/1 + /*skip length*/1;
for (int i = 0; i < SIZE(r.steps); ++i) {
const instruction& inst = r.steps.at(i);
if (inst.name == "left-click") {
trace("mem") << "storing 'left-click' event starting at " << Current_routine->alloc << end();
put(Memory, curr_address, /*tag for 'touch-event' variant of 'event' exclusive-container*/2);
put(Memory, curr_address+/*skip tag*/1+/*offset of 'type' in 'mouse-event'*/0, TB_KEY_MOUSE_LEFT);
put(Memory, curr_address+/*skip tag*/1+/*offset of 'row' in 'mouse-event'*/1, to_integer(inst.ingredients.at(0).name));
put(Memory, curr_address+/*skip tag*/1+/*offset of 'column' in 'mouse-event'*/2, to_integer(inst.ingredients.at(1).name));
curr_address += size_of_event();
}
else if (inst.name == "press") {
trace("mem") << "storing 'press' event starting at " << curr_address << end();
string key = inst.ingredients.at(0).name;
if (is_integer(key))
put(Memory, curr_address+1, to_integer(key));
else if (contains_key(Key, key))
put(Memory, curr_address+1, Key[key]);
else
raise << "assume-console: can't press '" << key << "'\n" << end();
if (get_or_insert(Memory, curr_address+1) < 256)
// these keys are in ascii
put(Memory, curr_address, /*tag for 'text' variant of 'event' exclusive-container*/0);
else