/* * Copyright (c) 2012-2026 Daniele Bartolini et al. * SPDX-License-Identifier: GPL-3.0-or-later */ #if CROWN_PLATFORM_LINUX extern uint gdk_x11_window_get_xid(Gdk.Window window); #elif CROWN_PLATFORM_WINDOWS extern uint gdk_win32_window_get_handle(Gdk.Window window); #endif namespace Crown { public class EditorView : Gtk.EventBox { public const Gtk.TargetEntry[] dnd_targets = { { "RESOURCE_PATH", Gtk.TargetFlags.SAME_APP, 0 }, }; // Data public RuntimeInstance _runtime; public Gtk.Allocation _allocation; public uint _resize_timer_id; public uint _enable_accels_id; public uint _tick_callback_id; public bool _mouse_left; public bool _mouse_middle; public bool _mouse_right; public uint _window_id; public uint _last_window_id; public Gee.HashMap _keys; public bool _input_enabled; public bool _drag_enter; public uint _drag_last_time; public int64 _motion_last_time; public const int MOTION_EVENTS_RATE = 75; public GLib.StringBuilder _buffer; public Gtk.EventControllerKey _controller_key; public Gtk.GestureMultiPress _gesture_click; public Gtk.EventControllerMotion _controller_motion; public Gtk.EventControllerScroll _controller_scroll; // Signals public signal void native_window_ready(uint window_id, int width, int height); public string key_to_string(uint k) { switch (k) { case Gdk.Key.w: return "w"; case Gdk.Key.a: return "a"; case Gdk.Key.s: return "s"; case Gdk.Key.d: return "d"; case Gdk.Key.q: return "q"; case Gdk.Key.e: return "e"; case Gdk.Key.Control_L: return "ctrl_left"; case Gdk.Key.Shift_L: return "shift_left"; case Gdk.Key.Alt_L: return "alt_left"; case Gdk.Key.Alt_R: return "alt_right"; default: return ""; } } public bool camera_modifier_pressed() { return _keys[Gdk.Key.Alt_L] || _keys[Gdk.Key.Alt_R] ; } public void camera_modifier_reset() { _keys[Gdk.Key.Alt_L] = false; _keys[Gdk.Key.Alt_R] = false; } public EditorView(RuntimeInstance runtime, bool input_enabled = true) { _runtime = runtime; _allocation = { 0, 0, 0, 0 }; _resize_timer_id = 0; _enable_accels_id = 0; _tick_callback_id = 0; _mouse_left = false; _mouse_middle = false; _mouse_right = false; _window_id = 0; _last_window_id = 0; _keys = new Gee.HashMap(); _keys[Gdk.Key.w] = false; _keys[Gdk.Key.a] = false; _keys[Gdk.Key.s] = false; _keys[Gdk.Key.d] = false; _keys[Gdk.Key.q] = false; _keys[Gdk.Key.e] = false; _keys[Gdk.Key.Control_L] = false; _keys[Gdk.Key.Shift_L] = false; _keys[Gdk.Key.Alt_L] = false; _keys[Gdk.Key.Alt_R] = false; _input_enabled = input_enabled; _drag_enter = false; _drag_last_time = 0; _motion_last_time = 0; _buffer = new GLib.StringBuilder(); // Widgets this.can_focus = true; this.events |= Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK | Gdk.EventMask.FOCUS_CHANGE_MASK | Gdk.EventMask.SCROLL_MASK ; this.focus_out_event.connect(on_event_box_focus_out_event); this.size_allocate.connect(on_size_allocate); if (input_enabled) { _controller_key = new Gtk.EventControllerKey(this); _controller_key.key_pressed.connect(on_key_pressed); _controller_key.key_released.connect(on_key_released); _gesture_click = new Gtk.GestureMultiPress(this); _gesture_click.set_button(0); _gesture_click.pressed.connect(on_button_pressed); _gesture_click.released.connect(on_button_released); _controller_motion = new Gtk.EventControllerMotion(this); _controller_motion.enter.connect(on_enter); _controller_motion.motion.connect(on_motion); _controller_scroll = new Gtk.EventControllerScroll(this, Gtk.EventControllerScrollFlags.BOTH_AXES); _controller_scroll.scroll.connect(on_scroll); } this.realize.connect(on_event_box_realized); this.set_visual(Gdk.Screen.get_default().get_system_visual()); this.events |= Gdk.EventMask.STRUCTURE_MASK; // map_event this.map_event.connect(() => { device_frame_delayed(16, _runtime); return Gdk.EVENT_PROPAGATE; }); Gtk.drag_dest_set(this, Gtk.DestDefaults.MOTION, dnd_targets, Gdk.DragAction.COPY); this.drag_data_received.connect(on_drag_data_received); this.drag_motion.connect(on_drag_motion); this.drag_drop.connect(on_drag_drop); this.drag_leave.connect(on_drag_leave); } public void on_drag_data_received(Gdk.DragContext context, int x, int y, Gtk.SelectionData data, uint info, uint time_) { // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_data_received.html unowned uint8[] raw_data = data.get_data_with_length(); if (raw_data.length == -1) return; string resource_path = (string)raw_data; string type = ResourceId.type(resource_path); string name = ResourceId.name(resource_path); if (type == OBJECT_TYPE_UNIT || type == OBJECT_TYPE_SOUND) { GLib.Application.get_default().activate_action("set-placeable", new GLib.Variant.tuple({ type, name })); int scale = this.get_scale_factor(); _runtime.send_script(LevelEditorApi.mouse_down(x*scale, y*scale)); } } public bool on_drag_motion(Gdk.DragContext context, int x, int y, uint _time) { // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_motion.html Gdk.Atom target; target = Gtk.drag_dest_find_target(this, context, null); if (target == Gdk.Atom.NONE) { Gdk.drag_status(context, 0, _time); } else { if (_drag_enter == false) { Gtk.drag_get_data(this, context, target, _time); _drag_enter = true; } if (_time - _drag_last_time >= 1000/MOTION_EVENTS_RATE) { // Drag motion events seem to fire at a very high frequency compared to regular // motion notify events. Limit them to 60 hz. _drag_last_time = _time; int scale = this.get_scale_factor(); _runtime.send_script(LevelEditorApi.set_mouse_state(x*scale , y*scale , _mouse_left , _mouse_middle , _mouse_right )); _runtime.send(DeviceApi.frame()); } } return true; } public bool on_drag_drop(Gdk.DragContext context, int x, int y, uint time_) { // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_drop.html int scale = this.get_scale_factor(); _runtime.send_script(LevelEditorApi.mouse_up(x*scale, y*scale)); GLib.Application.get_default().activate_action("cancel-place", null); _runtime.send(DeviceApi.frame()); Gtk.drag_finish(context, true, false, time_); return true; } public void on_drag_leave(Gdk.DragContext context, uint time_) { // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_leave.html _drag_enter = false; } public void on_button_released(int n_press, double x, double y) { uint button = _gesture_click.get_current_button(); int scale = this.get_scale_factor(); _mouse_left = button == Gdk.BUTTON_PRIMARY ? false : _mouse_left; _mouse_middle = button == Gdk.BUTTON_MIDDLE ? false : _mouse_middle; _mouse_right = button == Gdk.BUTTON_SECONDARY ? false : _mouse_right; _buffer.append(LevelEditorApi.set_mouse_state((int)x*scale , (int)y*scale , _mouse_left , _mouse_middle , _mouse_right )); if (button == Gdk.BUTTON_PRIMARY) _buffer.append(LevelEditorApi.mouse_up((int)x*scale, (int)y*scale)); if (camera_modifier_pressed()) { if (!_mouse_left || !_mouse_middle || !_mouse_right) _buffer.append("LevelEditor:camera_drag_start('idle')"); } else if (!_mouse_middle || !_mouse_right) { _buffer.append("LevelEditor:camera_drag_start('idle')"); bool is_flying = _tick_callback_id > 0; if (!_mouse_right && is_flying) { // Wait a little to prevent camera movement keys // from activating unwanted accelerators. _enable_accels_id = GLib.Timeout.add_full(GLib.Priority.DEFAULT, 300, on_enable_accels); if (_tick_callback_id != 0) { remove_tick_callback(_tick_callback_id); _tick_callback_id = 0; } } } if (_buffer.len != 0) { _runtime.send_script(_buffer.str); _buffer.erase(); _runtime.send(DeviceApi.frame()); } } public void on_button_pressed(int n_press, double x, double y) { uint button = _gesture_click.get_current_button(); int scale = this.get_scale_factor(); this.grab_focus(); _mouse_left = button == Gdk.BUTTON_PRIMARY ? true : _mouse_left; _mouse_middle = button == Gdk.BUTTON_MIDDLE ? true : _mouse_middle; _mouse_right = button == Gdk.BUTTON_SECONDARY ? true : _mouse_right; _buffer.append(LevelEditorApi.set_mouse_state((int)x*scale , (int)y*scale , _mouse_left , _mouse_middle , _mouse_right )); if (camera_modifier_pressed()) { if (_mouse_left) _buffer.append("LevelEditor:camera_drag_start('tumble')"); if (_mouse_middle) _buffer.append("LevelEditor:camera_drag_start('track')"); if (_mouse_right) _buffer.append("LevelEditor:camera_drag_start('dolly')"); } else if (_mouse_middle) { _buffer.append("LevelEditor:camera_drag_start('tumble')"); } else if (_mouse_right) { _buffer.append("LevelEditor:camera_drag_start('flythrough')"); if (_tick_callback_id == 0) _tick_callback_id = add_tick_callback(on_tick); if (_enable_accels_id > 0) GLib.Source.remove(_enable_accels_id); ((LevelEditorApplication)GLib.Application.get_default()).set_conflicting_accels(false); } if (button == Gdk.BUTTON_PRIMARY) _buffer.append(LevelEditorApi.mouse_down((int)x*scale, (int)y*scale)); if (_buffer.len != 0) { _runtime.send_script(_buffer.str); _buffer.erase(); _runtime.send(DeviceApi.frame()); } } public bool on_key_pressed(uint keyval, uint keycode, Gdk.ModifierType state) { if (keyval == Gdk.Key.Escape) GLib.Application.get_default().activate_action("cancel-place", null); if (keyval == Gdk.Key.Up) _buffer.append("LevelEditor:key_down(\"move_up\")"); if (keyval == Gdk.Key.Down) _buffer.append("LevelEditor:key_down(\"move_down\")"); if (keyval == Gdk.Key.Right) _buffer.append("LevelEditor:key_down(\"move_right\")"); if (keyval == Gdk.Key.Left) _buffer.append("LevelEditor:key_down(\"move_left\")"); if (_keys.has_key(keyval)) { if (!_keys[keyval]) { _buffer.append(LevelEditorApi.key_down(key_to_string(keyval))); if (keyval == Gdk.Key.w) _buffer.append("LevelEditor._camera.actions.forward = true;"); if (keyval == Gdk.Key.s) _buffer.append("LevelEditor._camera.actions.back = true;"); if (keyval == Gdk.Key.a) _buffer.append("LevelEditor._camera.actions.left = true;"); if (keyval == Gdk.Key.d) _buffer.append("LevelEditor._camera.actions.right = true;"); if (keyval == Gdk.Key.q) _buffer.append("LevelEditor._camera.actions.up = true;"); if (keyval == Gdk.Key.e) _buffer.append("LevelEditor._camera.actions.down = true;"); } _keys[keyval] = true; } if (_buffer.len != 0) { _runtime.send_script(_buffer.str); _buffer.erase(); _runtime.send(DeviceApi.frame()); } return Gdk.EVENT_PROPAGATE; } public void on_key_released(uint keyval, uint keycode, Gdk.ModifierType state) { if (_keys.has_key(keyval)) { if (_keys[keyval]) { _buffer.append(LevelEditorApi.key_up(key_to_string(keyval))); if (keyval == Gdk.Key.w) _buffer.append("LevelEditor._camera.actions.forward = false"); if (keyval == Gdk.Key.s) _buffer.append("LevelEditor._camera.actions.back = false"); if (keyval == Gdk.Key.a) _buffer.append("LevelEditor._camera.actions.left = false"); if (keyval == Gdk.Key.d) _buffer.append("LevelEditor._camera.actions.right = false"); if (keyval == Gdk.Key.q) _buffer.append("LevelEditor._camera.actions.up = false"); if (keyval == Gdk.Key.e) _buffer.append("LevelEditor._camera.actions.down = false"); } _keys[keyval] = false; } if (_buffer.len != 0) { _runtime.send_script(_buffer.str); _buffer.erase(); _runtime.send(DeviceApi.frame()); } } public void on_motion(double x, double y) { int64 now = GLib.get_monotonic_time(); if (now - _motion_last_time >= (1000*1000)/MOTION_EVENTS_RATE) { _motion_last_time = now; int scale = this.get_scale_factor(); _runtime.send_script(LevelEditorApi.set_mouse_state((int)x*scale , (int)y*scale , _mouse_left , _mouse_middle , _mouse_right )); _runtime.send(DeviceApi.frame()); } } public void on_scroll(double dx, double dy) { if (_keys[Gdk.Key.Shift_L]) { _runtime.send_script(LevelEditorApi.mouse_wheel(-dy)); } else { _runtime.send_script("LevelEditor:camera_drag_start_relative('dolly')"); _runtime.send_script("LevelEditor._camera:update(1,0,%.17f,1,1)".printf(-dy * 32.0)); _runtime.send_script("LevelEditor:camera_drag_start('idle')"); _runtime.send(DeviceApi.frame()); } } public bool on_event_box_focus_out_event(Gdk.EventFocus ev) { camera_modifier_reset(); _keys[Gdk.Key.Control_L] = false; _keys[Gdk.Key.Shift_L] = false; _runtime.send_script(LevelEditorApi.key_up(key_to_string(Gdk.Key.Control_L))); _runtime.send_script(LevelEditorApi.key_up(key_to_string(Gdk.Key.Shift_L))); return Gdk.EVENT_PROPAGATE; } public void on_size_allocate(Gtk.Allocation ev) { int scale = this.get_scale_factor(); if (_allocation.x == ev.x && _allocation.y == ev.y && _allocation.width == ev.width && _allocation.height == ev.height ) return; if (_last_window_id != _window_id) { _last_window_id = _window_id; native_window_ready(_window_id, ev.width*scale, ev.height*scale); } _allocation = ev; _runtime.send(DeviceApi.resize(_allocation.width*scale, _allocation.height*scale)); // Ensure there is some delay between the last resize() and the last frame(). if (_resize_timer_id == 0) { _resize_timer_id = GLib.Timeout.add_full(GLib.Priority.DEFAULT, 200, () => { _runtime.send(DeviceApi.frame()); _resize_timer_id = 0; return GLib.Source.REMOVE; }); } } public void on_event_box_realized() { this.get_window().ensure_native(); #if CROWN_PLATFORM_LINUX this.get_display().sync(); _window_id = gdk_x11_window_get_xid(this.get_window()); #elif CROWN_PLATFORM_WINDOWS _window_id = gdk_win32_window_get_handle(this.get_window()); #endif } public void on_enter(double x, double y) { this.grab_focus(); } public bool on_tick(Gtk.Widget widget, Gdk.FrameClock frame_clock) { _runtime.send(DeviceApi.frame()); return GLib.Source.CONTINUE; } public bool on_enable_accels() { ((LevelEditorApplication)GLib.Application.get_default()).set_conflicting_accels(true); _enable_accels_id = 0; return GLib.Source.REMOVE; } } } /* namespace Crown */