def render_variable_cell( id: str, value: T, formatter: VariableFormatter, cell_size: Tuple[int, int], is_selected: bool, frame: Optional[int] = None, highlight_range: Optional[Tuple[range, ig.Color4f]] = None, ) -> Tuple[Maybe[T], bool, bool, bool]: ig.push_id(id) window_pos = ig.get_window_position() item_spacing = ig.get_style().item_spacing cell_cursor_pos = ig.get_cursor_pos() cell_cursor_pos = ( cell_cursor_pos.x + window_pos.x - item_spacing.x, cell_cursor_pos.y + window_pos.y - ig.get_scroll_y() - item_spacing.y, ) if highlight_range is not None: assert frame is not None frames, color = highlight_range margin = 5 offset_top = margin if frame == frames.start else 0 offset_bottom = margin if frame == frames.stop - 1 else 0 dl = ig.get_window_draw_list() dl.add_rect_filled( cell_cursor_pos[0] + margin, cell_cursor_pos[1] + offset_top, cell_cursor_pos[0] + cell_size[0] - margin, cell_cursor_pos[1] + cell_size[1] - offset_bottom, ig.get_color_u32_rgba(*color), ) changed_data, selected, pressed = render_variable_value( 'value', value, formatter, ( cell_size[0] - 2 * item_spacing.x, cell_size[1] - 2 * item_spacing.y, ), highlight=is_selected, ) clear_edit = ig.is_item_hovered() and ig.is_mouse_down(2) ig.pop_id() return changed_data, clear_edit, selected, pressed
def input_down_gradual(name: str, until_full: float) -> float: ig.push_id('ctrl-down-gradual-' + name) down_start: Ref[Optional[float]] = use_state('down-start', None) down = _input_down_uncond(name) if not down: down_start.value = None result = 0.0 elif down_start.value is None: down_start.value = time.time() result = 0.0 else: result = min((time.time() - down_start.value) / until_full, 1) ig.pop_id() return result if ig.global_keyboard_capture() else 0.0
def render_variable_value( id: str, value: T, formatter: VariableFormatter, size: Tuple[int, int], highlight: bool = False, ) -> Tuple[Maybe[T], bool, bool]: ig.push_id(id) if isinstance(formatter, TextFormatter): result = _render_text(value, formatter, size, highlight) elif isinstance(formatter, CheckboxFormatter): result = _render_checkbox(value, formatter, size, highlight) else: raise NotImplementedError(formatter) ig.pop_id() return result
def render_key_binding_settings(id: str) -> None: ig.push_id(id) begin_binding_form() ig.text('Frame advance:') button_width = 0 binding_button('frame-prev-fast', '-10', button_width) binding_button('frame-prev', '-1', button_width) binding_button('frame-prev-alt', '-1', button_width) binding_button('frame-next', '+1', button_width) binding_button('frame-next-alt', '+1', button_width) binding_button('frame-next-fast', '+10', button_width) ig.dummy(1, 5) ig.text('3D movement:') button_width = 70 binding_button('3d-camera-move-f', 'Forward', button_width) binding_button('3d-camera-move-b', 'Back', button_width) binding_button('3d-camera-move-l', 'Left', button_width) binding_button('3d-camera-move-r', 'Right', button_width) binding_button('3d-camera-move-u', 'Up', button_width) binding_button('3d-camera-move-d', 'Down', button_width) ig.dummy(1, 5) ig.text('Playback:') button_width = 90 binding_button('playback-play', 'Play', button_width) binding_button('playback-rewind', 'Rewind', button_width) binding_button('playback-speed-up', 'Speed up', button_width) binding_button('playback-slow-down', 'Slow down', button_width) ig.text_colored( 'Tip: Use the frame advance keys\nduring playback to change speed\ntemporarily.', 1.0, 1.0, 1.0, 0.5, ) ig.dummy(200, 1) ig.pop_id()
def render_frame_slider( id: str, current_frame: int, num_frames: int, loaded_frames: List[int] = [], ) -> Maybe[int]: ig.push_id(id) pos = ig.get_cursor_pos() pos = ( pos[0] + ig.get_window_position()[0], pos[1] + ig.get_window_position()[1] - ig.get_scroll_y(), ) width = ig.get_content_region_available_width() ig.push_item_width(width) changed, new_frame = ig.slider_int( '##slider', current_frame, 0, num_frames - 1, ) ig.pop_item_width() dl = ig.get_window_draw_list() for frame in loaded_frames: line_pos = pos[0] + frame / num_frames * width dl.add_line( line_pos, pos[1] + 13, line_pos, pos[1] + 18, ig.get_color_u32_rgba(1, 0, 0, 1), ) ig.pop_id() if changed: return Just(new_frame) else: return None
def input_pressed_repeat(name: str, delay: float, per_second: float) -> int: ig.push_id('ctrl-pressed-repeat-' + name) down_start: Ref[Optional[float]] = use_state('down-start', None) counted = use_state('counted', 0) down = _input_down_uncond(name) if not down: down_start.value = None count = 0 elif down_start.value is None: down_start.value = time.time() counted.value = 0 count = 1 else: held = time.time() - down_start.value total_count = int(per_second * max(held - delay, 0)) count = total_count - counted.value counted.value = total_count ig.pop_id() return count if ig.global_keyboard_capture() else 0
def render_input_text_with_error( id: str, value: str, buffer_size: int, width: int, validate: Callable[[str], T], ) -> Maybe[T]: ig.push_id(id) cursor_pos = ig.get_cursor_pos() cursor_pos = ( ig.get_window_position()[0] + cursor_pos[0], ig.get_window_position()[1] + cursor_pos[1] - ig.get_scroll_y(), ) ig.push_item_width(width) changed, new_value = ig.input_text('##input', value, buffer_size) ig.pop_item_width() result_value = None if changed: try: result_value = Just(validate(new_value)) except: # TODO: Show error message dl = ig.get_window_draw_list() dl.add_rect( cursor_pos[0], cursor_pos[1], cursor_pos[0] + width, cursor_pos[1] + ig.get_text_line_height() + 2 * ig.get_style().frame_padding[1], ig.get_color_u32_rgba(1, 0, 0, 1), ) new_value = value ig.pop_id() return result_value
def render(self) -> None: ig.push_id(str(epoch)) # if ig.is_key_pressed(ord('`')): # self.show_debug_pane = not self.show_debug_pane self.handle_controller() prev_frame_time = use_state_with('prev-frame-time', time.time) accum_time = use_state('accum-time', 0.0) now = time.time() accum_time.value += now - prev_frame_time.value prev_frame_time.value = now play_speed = self.model.play_speed if play_speed == 0.0: accum_time.value = 0 else: target_fps = 30 * abs(play_speed) target_dt = 1 / target_fps updates = 0 while accum_time.value >= target_dt and updates < 20: accum_time.value -= target_dt self.model.selected_frame += 1 if play_speed > 0 else -1 self.handle_controller() updates += 1 ig_window_size = ig.get_window_size() window_size = (int(ig_window_size.x), int(ig_window_size.y)) ig.columns(2) self.render_left_column(window_size) ig.next_column() self.render_right_column() ig.columns(1) ig.pop_id()
def render_controller_settings(id: str) -> None: ig.push_id(id) begin_binding_form() def cardinal(prefix: str) -> None: ig.dummy(45, 1) ig.same_line() binding_button('n64-' + prefix + '^', prefix + '^') binding_button('n64-' + prefix + '<', prefix + '<') ig.same_line() ig.set_cursor_pos((110, ig.get_cursor_pos().y)) binding_button('n64-' + prefix + '>', prefix + '>') ig.dummy(45, 1) ig.same_line() binding_button('n64-' + prefix + 'v', prefix + 'v') binding_button('n64-A', 'A') binding_button('n64-B', 'B') binding_button('n64-Z', 'Z') ig.dummy(1, 5) binding_button('n64-L', 'L') binding_button('n64-R', 'R') binding_button('n64-S', 'S') ig.dummy(1, 5) cardinal('') ig.dummy(1, 15) cardinal('C') ig.dummy(1, 15) cardinal('D') ig.dummy(200, 1) ig.pop_id()
def handle_controller(self) -> None: ig.push_id('controller-inputs') buttons_enabled = use_state('buttons-enabled', False) stick_enabled = use_state('stick-enabled', False) def add_callbacks() -> Ref[bool]: input_edit = Ref(False) def disable_controller(*args, **kwargs) -> None: if not input_edit.value: buttons_enabled.value = False stick_enabled.value = False self.model.on_edit(disable_controller) def frame_change(*args, **kwargs) -> None: if self.model.play_speed == 0.0: disable_controller() self.model.on_selected_frame_change(frame_change) return input_edit input_edit = use_state_with('initialize', add_callbacks).value prev_play_speed = use_state('prev-play-speed', 0.0) if self.model.play_speed != prev_play_speed.value: buttons_enabled.value = False stick_enabled.value = False prev_play_speed.value = self.model.play_speed controller_button_values = { 'input-button-a': input_down('n64-A'), 'input-button-b': input_down('n64-B'), 'input-button-z': input_down('n64-Z'), 'input-button-s': input_down('n64-S'), 'input-button-l': input_down('n64-L'), 'input-button-r': input_down('n64-R'), 'input-button-cu': input_down('n64-C^'), 'input-button-cl': input_down('n64-C<'), 'input-button-cr': input_down('n64-C>'), 'input-button-cd': input_down('n64-Cv'), 'input-button-du': input_down('n64-D^'), 'input-button-dl': input_down('n64-D<'), 'input-button-dr': input_down('n64-D>'), 'input-button-dd': input_down('n64-Dv'), } if any(controller_button_values.values()): buttons_enabled.value = True stick_enabled.value = True for variable_name, new_button_value in controller_button_values.items(): variable = Variable(variable_name).with_frame(self.model.selected_frame) button_value = self.model.get(variable) if buttons_enabled.value and button_value != new_button_value: input_edit.value = True self.model.set(variable, new_button_value) input_edit.value = False controller_stick_values = ( input_float('n64->') - input_float('n64-<'), input_float('n64-^') - input_float('n64-v'), ) # Require a larger magnitude for enabling controller since dead zone may be too small if any(abs(v) > 0.1 for v in controller_stick_values): stick_enabled.value = True buttons_enabled.value = True if stick_enabled.value: stick_x_var = Variable('input-stick-x').with_frame(self.model.selected_frame) stick_y_var = Variable('input-stick-y').with_frame(self.model.selected_frame) new_stick = self.compute_stick_from_controller(*controller_stick_values) stick = (self.model.get(stick_x_var), self.model.get(stick_y_var)) if stick != new_stick: input_edit.value = True self.model.set(stick_x_var, new_stick[0]) self.model.set(stick_y_var, new_stick[1]) input_edit.value = False ig.pop_id()
def render_game_view_birds_eye( id: str, framebuffer_size: Tuple[int, int], model: Model, wall_hitbox_radius: float, hovered_surface: Optional[int], hidden_surfaces: Set[int], ) -> Optional[int]: ig.push_id(id) # TODO: Should zoom in on mouse when uncentered mouse_state = use_state('mouse-state', MouseTracker()).value zoom = use_state('zoom', -4.5) target: Ref[Optional[Tuple[float, float]]] = use_state('target', None) pos_y: Ref[Optional[float]] = use_state('pos-y', None) drag_amount = mouse_state.get_drag_amount() zoom.value += mouse_state.get_wheel_amount() / 5 world_span_x = 200 / math.pow(2, zoom.value) viewport = get_viewport(framebuffer_size) mario_pos = get_mario_pos(model) # Camera xz camera_xz = (mario_pos[0], mario_pos[2]) if target.value is not None: camera_xz = target.value if drag_amount != (0.0, 0.0): world_span_z = world_span_x * viewport.width / viewport.height if target.value is None: target.value = (mario_pos[0], mario_pos[2]) target.value = ( camera_xz[0] + drag_amount[1] * world_span_x / viewport.height, camera_xz[1] - drag_amount[0] * world_span_z / viewport.width, ) camera_xz = target.value if ig.disableable_button('Lock to Mario', enabled=target.value is not None): target.value = None # Camera y camera_y = mario_pos[1] + 500 if pos_y.value is None else pos_y.value ig.set_cursor_pos((viewport.width - 100, 10)) ig.begin_child('##y-slider') new_y, reset = render_pos_y_slider('y-slider', camera_y, mario_pos[1]) if reset: pos_y.value = None elif new_y is not None: pos_y.value = new_y camera_y = pos_y.value ig.end_child() camera = core.BirdsEyeCamera() camera.pos = (camera_xz[0], camera_y, camera_xz[1]) camera.span_y = world_span_x # Mouse xz mouse_world_pos = get_mouse_world_pos_birds_eye(camera) mouse_ray: Optional[Tuple[Vec3f, Vec3f]] if mouse_world_pos is not None: ig.set_cursor_pos((10, viewport.height - 25)) ig.text('(x, z) = (%.3f, %.3f)' % mouse_world_pos) mouse_ray = ((mouse_world_pos[0], camera.pos[1], mouse_world_pos[1]), (0, -1, 0)) else: mouse_ray = None if mouse_ray is None: new_hovered_surface = None else: new_hovered_surface = trace_ray(model, mouse_ray) render_game( model, viewport, camera, False, wall_hitbox_radius, hovered_surface=hovered_surface, hidden_surfaces=hidden_surfaces, ) ig.pop_id() return new_hovered_surface
def render_joystick_control( id: str, stick_x: float, stick_y: float, shape='square', ) -> Optional[Tuple[float, float]]: ig.push_id(id) state = use_state('', JoystickControlState()).value dl = ig.get_window_draw_list() padding = 10 content_region = ig.get_content_region_available() size = min( content_region.x - ig.get_style().scrollbar_size - 2 * padding, content_region.y - 2 * padding, 200, ) size = max(size, 100) initial_cursor_pos = ig.get_cursor_pos() top_left = ( initial_cursor_pos[0] + ig.get_window_position()[0] - ig.get_scroll_x() + padding, initial_cursor_pos[1] + ig.get_window_position()[1] - ig.get_scroll_y() + padding, ) background_color = ig.get_color_u32_rgba(0, 0, 0, 0.3) if shape == 'square': dl.add_rect_filled( top_left[0], top_left[1], top_left[0] + size, top_left[1] + size, background_color, ) elif shape == 'circle': dl.add_circle_filled( top_left[0] + size / 2, top_left[1] + size / 2, size / 2, background_color, num_segments=32, ) result = None if state.active and ig.is_mouse_down(): new_offset = state.get_value(ig.get_mouse_drag_delta(lock_threshold=0)) new_stick_x = new_offset[0] / size * 2 - 1 new_stick_y = (1 - new_offset[1] / size) * 2 - 1 if shape == 'square': new_stick_x = min(max(new_stick_x, -1), 1) new_stick_y = min(max(new_stick_y, -1), 1) elif shape == 'circle': mag = math.sqrt(new_stick_x**2 + new_stick_y**2) if mag > 1: new_stick_x /= mag new_stick_y /= mag if (new_stick_x, new_stick_y) != (stick_x, stick_y): stick_x, stick_y = new_stick_x, new_stick_y result = (stick_x, stick_y) offset = ( (stick_x + 1) / 2 * size, (1 - (stick_y + 1) / 2) * size, ) dl.add_line( top_left[0] + size / 2, top_left[1] + size / 2, top_left[0] + offset[0], top_left[1] + offset[1], ig.get_color_u32_rgba(1, 1, 1, 0.5), ) button_size = 20 button_pos = ( padding + initial_cursor_pos[0] + offset[0] - button_size / 2, padding + initial_cursor_pos[1] + offset[1] - button_size / 2, ) ig.set_cursor_pos(button_pos) ig.button('##joystick-button', button_size, button_size) ig.set_cursor_pos(( initial_cursor_pos[0], initial_cursor_pos[1] + size + 2 * padding, )) if ig.is_item_active(): state.set_active(offset) else: state.reset() ig.pop_id() return result
def render_tabs( id: str, tabs: List[TabInfo], open_tab_index: Optional[int] = None, allow_windowing=False, ) -> Tuple[Optional[int], Optional[int]]: ig.push_id(id) root_id = get_local_state_id_stack() ig.columns(2) closed_tab = None rendered = use_state('rendered', False) if not rendered.value: rendered.value = True ig.set_column_width(-1, 120) if len(tabs) == 0: ig.pop_id() return None, closed_tab selected_tab_index = use_state_with('selected-tab-index', lambda: open_tab_index or 0) selected_tab_id = use_state_with('selected-tab', lambda: tabs[selected_tab_index.value].id) if open_tab_index is not None: selected_tab_index.value = open_tab_index selected_tab_id.value = tabs[open_tab_index].id windowed_tabs = use_state('windowed-tabs', cast(Set[str], set())).value # TODO: Change selected tab if windowed # Handle deletion/insertion if selected_tab_index.value >= len(tabs): selected_tab_index.value = len(tabs) - 1 if tabs[selected_tab_index.value].id != selected_tab_id.value: matching_indices = [ i for i in range(len(tabs)) if tabs[i].id == selected_tab_id.value ] if len(matching_indices) > 0: selected_tab_index.value = matching_indices[0] else: selected_tab_id.value = tabs[selected_tab_index.value].id ig.begin_child('tabs') for i, tab in enumerate(tabs): if tab.id in windowed_tabs: continue _, selected = ig.selectable( tab.label + '##tab-' + tab.id, selected_tab_id.value == tab.id, ) if selected: selected_tab_index.value = i selected_tab_id.value = tab.id if tab.closable and ig.is_item_hovered() and ig.is_mouse_clicked(2): closed_tab = i if allow_windowing or tab.closable: if ig.begin_popup_context_item(f'##ctx-{tab.id}'): if allow_windowing and ig.selectable('Pop out')[0]: windowed_tabs.add(tab.id) if tab.closable and ig.selectable('Close')[0]: closed_tab = i ig.end_popup_context_item() ig.end_child() ig.next_column() ig.begin_child('content', flags=ig.WINDOW_HORIZONTAL_SCROLLING_BAR) tab = tabs[selected_tab_index.value] if tab.id not in windowed_tabs: push_local_state_rebase(('rebase-tabs', ) + root_id) tab.render(tab.id) # type: ignore pop_local_state_rebase() ig.end_child() ig.columns(1) for tab_id in set(windowed_tabs): matching = [tab for tab in tabs if tab.id == tab_id] if len(matching) == 0: windowed_tabs.remove(tab.id) continue tab = matching[0] ig.set_next_window_size(*ig.get_window_size(), ig.ONCE) ig.set_next_window_position(*ig.get_window_position(), ig.ONCE) ig.push_style_color(ig.COLOR_WINDOW_BACKGROUND, 0.06, 0.06, 0.06, 0.94) _, opened = ig.begin( tab.label + '##window-' + tab.id, closable=True, flags=ig.WINDOW_HORIZONTAL_SCROLLING_BAR, ) push_local_state_rebase(('rebase-tabs', ) + root_id) tab.render(tab.id) # type: ignore pop_local_state_rebase() ig.end() ig.pop_style_color() if not opened: windowed_tabs.remove(tab.id) ig.pop_id() return ( None if open_tab_index == selected_tab_index.value else selected_tab_index.value, closed_tab, )