class Button(Component, ABC): hover: RV[bool] = rv.new_view() active: RV[bool] = rv.new_view() def __init__(self, context: Context, visible: bool = True) -> None: super().__init__(context, visible) # noinspection PyTypeChecker self.hover = rx.merge( self.on_mouse_over.pipe(ops.map(lambda _: True)), self.on_mouse_out.pipe(ops.map(lambda _: False))).pipe(ops.start_with(False)) mouse = MouseInput.input(self) # noinspection PyTypeChecker self.active = self.on_mouse_down.pipe( ops.filter(lambda e: e.button == MouseButton.LEFT), ops.map(lambda _: rx.concat(rx.of(True), mouse.on_button_release(MouseButton.LEFT).pipe( ops.take(1), ops.map(lambda _: False)))), ops.exclusive(), ops.start_with(False)) def dispatch_event(self, event: Event) -> None: if isinstance(event, PropagatingEvent): event.stop_propagation() super().dispatch_event(event) @property def style_fallback_prefixes(self) -> Iterable[str]: return chain(["Button"], super().style_fallback_prefixes)
class FixtureContext(Context): window_size: RV[Dimension] = rv.new_view() def __init__(self, size: Dimension, toolkit: FixtureToolkit, look_and_feel: Optional[LookAndFeel] = None, font_options: Optional[FontOptions] = None, window_manager: Optional[WindowManager] = None, error_handler: Optional[ErrorHandler] = None) -> None: if size is None: raise ValueError("Argument 'size' is required.") # noinspection PyTypeChecker self.window_size = rx.of(size) super().__init__(toolkit, look_and_feel, font_options, window_manager, error_handler)
class BlenderKeyInput(KeyInput, ReactiveObject, EventLoopAware): pressed: RV[Set[int]] = rv.new_view() def __init__(self, context: BlenderContext) -> None: super().__init__(context) self._activeInputs = Subject() # noinspection PyTypeChecker self.pressed = self._activeInputs.pipe( ops.start_with({}), ops.map(lambda s: set(s.keys()))) def process(self) -> None: self._activeInputs.on_next(keyboard.activeInputs) def dispose(self) -> None: super().dispose() self.execute_safely(self._activeInputs.dispose)
class WindowManager(Drawable, ErrorHandlerSupport, ReactiveObject): windows: RV[Sequence[Window]] = rv.new_view() def __init__(self, error_handler: ErrorHandler) -> None: if error_handler is None: raise ValueError("Argument 'error_handler' is required.") super().__init__() self._error_handler = error_handler self._added_window = Subject() self._removed_window = Subject() changed_window = rx.merge( self._added_window.pipe(ops.map(lambda v: (v, True))), self._removed_window.pipe(ops.map(lambda v: (v, False)))) def on_window_change(windows: Tuple[Window, ...], event: Tuple[Window, bool]): (window, added) = event if added and window not in windows: return windows + (window,) elif not added and window in windows: return tuple(c for c in windows if c is not window) # noinspection PyTypeChecker self.windows = changed_window.pipe( ops.scan(on_window_change, ()), ops.start_with(()), ops.distinct_until_changed()) def add(self, window: Window) -> None: if window is None: raise ValueError("Argument 'window' is required.") self._added_window.on_next(window) def remove(self, window: Window) -> None: if window is None: raise ValueError("Argument 'window' is required.") self._removed_window.on_next(window) def window_at(self, location: Point) -> Maybe[Window]: if location is None: raise ValueError("Argument 'location' is required.") try: # noinspection PyTypeChecker children: Iterator[Window] = reversed(self.windows) return Some(next(c for c in children if c.bounds.contains(location))) except StopIteration: return Nothing def draw(self, g: Graphics) -> None: # noinspection PyTypeChecker for window in self.windows: window.validate() window.draw(g) @property def error_handler(self) -> ErrorHandler: return self._error_handler def dispose(self) -> None: # noinspection PyTypeChecker for window in self.windows: self.execute_safely(window.dispose) super().dispose()
class Context(EventLoopAware, ReactiveObject, InputLookup, ErrorHandlerSupport, ABC): window_size: RV[Dimension] surface: RV[Surface] = rv.new_view() graphics: RV[Graphics] = surface.map(lambda c, s: c.create_graphics(s)) def __init__(self, toolkit: Toolkit, look_and_feel: Optional[LookAndFeel] = None, font_options: Optional[FontOptions] = None, window_manager: Optional[WindowManager] = None, error_handler: Optional[ErrorHandler] = None) -> None: if toolkit is None: raise ValueError("Argument 'toolkit' is required.") super().__init__() from alleycat.ui import WindowManager from alleycat.ui.glass import GlassLookAndFeel self._toolkit = toolkit self._look_and_feel = Maybe.from_optional(look_and_feel).or_else_call(lambda: GlassLookAndFeel(toolkit)) self._font_options = Maybe.from_optional(font_options).or_else_call( lambda: FontOptions(antialias=ANTIALIAS_SUBPIXEL, hint_style=HINT_STYLE_FULL)) self._error_handler = Maybe.from_optional(error_handler).value_or(toolkit.error_handler) self._window_manager = Maybe.from_optional(window_manager) \ .or_else_call(lambda: WindowManager(self.error_handler)) inputs = toolkit.create_inputs(self) assert inputs is not None self._inputs = {i.id: i for i in inputs} self._pollers = [i for i in inputs if isinstance(i, EventLoopAware)] # noinspection PyTypeChecker self.surface = self.observe("window_size").pipe(ops.map(self.toolkit.create_surface)) old_surface = self.observe("surface").pipe( ops.pairwise(), ops.map(lambda s: s[0]), ops.take_until(self.on_dispose)) old_surface.subscribe(Surface.finish, on_error=self.error_handler) @property def toolkit(self) -> Toolkit: return self._toolkit @property def inputs(self) -> Mapping[str, Input]: return self._inputs @property def look_and_feel(self) -> LookAndFeel: return self._look_and_feel @property def font_options(self) -> FontOptions: return self._font_options @property def window_manager(self) -> WindowManager: return self._window_manager @property def error_handler(self) -> ErrorHandler: return self._error_handler # noinspection PyMethodMayBeStatic def create_graphics(self, surface: Surface): if surface is None: raise ValueError("Argument 'surface' is required.") g = Graphics(surface) g.set_antialias(ANTIALIAS_BEST) g.set_font_options(self.font_options) return g def process(self) -> None: self.execute_safely(self.process_inputs) self.execute_safely(self.process_draw) def process_inputs(self) -> None: for poller in self._pollers: self.execute_safely(poller.process) def process_draw(self) -> None: (width, height) = self.window_size.tuple g: Graphics = self.graphics op = g.get_operator() g.rectangle(0, 0, width, height) g.set_source_rgba(0, 0, 0, 0) g.set_operator(OPERATOR_CLEAR) g.fill() g.set_operator(op) self.window_manager.draw(g) self.surface.flush() def dispatcher_at(self, location: Point) -> Maybe[EventDispatcher]: if location is None: raise ValueError("Argument 'location' is required.") return self._window_manager.window_at(location).bind(lambda w: w.component_at(location)) def dispose(self) -> None: super().dispose() self.execute_safely(self._window_manager.dispose) for i in self.inputs.values(): self.execute_safely(i.dispose)
class Container(Component): children: RV[Sequence[Component]] = rv.new_view() def __init__(self, context: Context, layout: Optional[Layout] = None, visible: bool = True): from .layout import AbsoluteLayout self._layout = Maybe.from_optional(layout).or_else_call(AbsoluteLayout) self._layout_pending = True self._layout_running = False # noinspection PyTypeChecker self.children = self.layout.observe("children").pipe( ops.map( lambda children: tuple(map(lambda c: c.component, children)))) super().__init__(context, visible) self.observe("size") \ .pipe(ops.filter(lambda _: self.visible), ops.distinct_until_changed()) \ .subscribe(lambda _: self.request_layout(), on_error=self.error_handler) @property def layout(self) -> Layout: return self._layout def validate(self, force: bool = False) -> None: if self.visible and (not self.valid or force): self.request_layout() # noinspection PyTypeChecker for child in self.children: child.validate(force) super().validate(force) def invalidate(self) -> None: if not self._layout_running: super().invalidate() @property def layout_pending(self) -> bool: return self._layout_pending or not self.valid def request_layout(self) -> None: self._layout_pending = True def perform_layout(self) -> None: self._layout_running = True try: self.layout.perform(self.bounds.copy(x=0, y=0)) except BaseException as e: self.context.error_handler(e) self._layout_pending = False self._layout_running = False def component_at(self, location: Point) -> Maybe[Component]: if location is None: raise ValueError("Argument 'location' is required.") if self.bounds.contains(location): try: # noinspection PyTypeChecker children: Iterator[Component] = reversed(self.children) child = next(c for c in children if c.bounds.contains(location - self.location)) if isinstance(child, Container): return child.component_at(location - self.location) else: return Some(child) except StopIteration: return Some(self) return Nothing def add(self, child: Component, *args, **kwargs) -> None: child.parent.map(lambda p: p.remove(child)) self.layout.add(child, *args, **kwargs) child.parent = Some(self) def remove(self, child: Component) -> None: self.layout.remove(child) child.parent = Nothing def draw(self, g: Graphics) -> None: if self.layout_pending: self.perform_layout() super().draw(g) def draw_component(self, g: Graphics) -> None: super().draw_component(g) self.draw_children(g) cast(ContainerUI, self.ui).post_draw(g, self) def draw_children(self, g: Graphics) -> None: ui = cast(ContainerUI, self.ui) (cx, cy, cw, ch) = ui.content_bounds(self).tuple g.save() g.rectangle(cx, cy, cw, ch) g.clip() try: # noinspection PyTypeChecker for child in self.children: child.draw(g) except BaseException as e: self.error_handler(e) g.restore() def dispose(self) -> None: # noinspection PyTypeChecker for child in self.children: self.execute_safely(child.dispose) self.execute_safely(self.layout.dispose) super().dispose()
class BlenderContext(Context): window_size: RV[Dimension] = rv.new_view() batch: RV[GPUBatch] = window_size.map(lambda c, s: c.create_batch(s)) buffer: RV[GPUBatch] = window_size.map(lambda c, s: c.create_batch(s)) def __init__(self, toolkit: BlenderToolkit, look_and_feel: Optional[LookAndFeel] = None, font_options: Optional[FontOptions] = None, window_manager: Optional[WindowManager] = None, error_handler: Optional[ErrorHandler] = None) -> None: super().__init__(toolkit, look_and_feel, font_options, window_manager, error_handler) resolution = Dimension(bge.render.getWindowWidth(), bge.render.getWindowHeight()) self._resolution = BehaviorSubject(resolution) # noinspection PyTypeChecker self.window_size = self._resolution.pipe(ops.distinct_until_changed()) self._shader = cast(GPUShader, gpu.shader.from_builtin("2D_IMAGE")) if use_viewport_render: # noinspection PyArgumentList self._draw_handler = SpaceView3D.draw_handler_add( self.process, (), "WINDOW", "POST_PIXEL") else: self._draw_handler = None bge.logic.getCurrentScene().post_draw.append(self.process) # noinspection PyTypeChecker self._texture = Buffer(bgl.GL_INT, 1) bgl.glGenTextures(1, self.texture) @property def shader(self) -> GPUShader: return self._shader @property def texture(self) -> Buffer: return self._texture def create_batch(self, size: Dimension) -> GPUBatch: if size is None: raise ValueError("Argument 'size' is required.") points = Bounds(0, 0, size.width, size.height).points vertices = tuple(map(lambda p: p.tuple, map(self.translate, points))) coords = ((0, 0), (1, 0), (1, 1), (0, 1)) indices = {"pos": vertices, "texCoord": coords} return batch_for_shader(self.shader, "TRI_FAN", indices) def translate(self, point: Point) -> Point: if point is None: raise ValueError("Argument 'point' is required.") return point.copy(y=self.window_size.height - point.y) # noinspection PyTypeChecker def process_draw(self) -> None: width = bge.render.getWindowWidth() height = bge.render.getWindowHeight() self._resolution.on_next(Dimension(width, height)) super().process_draw() data = self.surface.get_data() source = bgl.Buffer(bgl.GL_BYTE, width * height * 4, data) bgl.glEnable(bgl.GL_BLEND) bgl.glActiveTexture(bgl.GL_TEXTURE0) # noinspection PyUnresolvedReferences bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.texture[0]) bgl.glTexImage2D(bgl.GL_TEXTURE_2D, 0, bgl.GL_SRGB_ALPHA, width, height, 0, bgl.GL_BGRA, bgl.GL_UNSIGNED_BYTE, source) bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MIN_FILTER, bgl.GL_NEAREST) bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MAG_FILTER, bgl.GL_NEAREST) self.shader.bind() self.shader.uniform_int("image", 0) self.batch.draw(self.shader) bgl.glDeleteBuffers(1, source) def dispose(self) -> None: if self._draw_handler: # noinspection PyArgumentList SpaceView3D.draw_handler_remove(self._draw_handler, "WINDOW") else: bge.logic.getCurrentScene().post_draw.remove(self.process) bgl.glDeleteTextures(1, self.texture) # noinspection PyTypeChecker bgl.glDeleteBuffers(1, self.texture) super().dispose()
class BlenderMouseInput(MouseInput, ReactiveObject, EventLoopAware): position: RV[Point] = rv.new_view() buttons: RV[int] = rv.new_view() def __init__(self, context: BlenderContext) -> None: super().__init__(context) self._position = Subject() self._activeInputs = Subject() # noinspection PyTypeChecker self.position = self._position.pipe( ops.distinct_until_changed(), ops.map(lambda v: tuple( p * s for p, s in zip(v, context.window_size.tuple))), ops.map(Point.from_tuple), ops.share()) codes = { MouseButton.LEFT: bge.events.LEFTMOUSE, MouseButton.MIDDLE: bge.events.MIDDLEMOUSE, MouseButton.RIGHT: bge.events.RIGHTMOUSE } def pressed(e: SCA_InputEvent) -> bool: return KX_INPUT_ACTIVE in e.status or KX_INPUT_JUST_ACTIVATED in e.status def value_for(button: MouseButton) -> Observable: code = codes[button] return self._activeInputs.pipe( ops.start_with({}), ops.map(lambda i: code in i and pressed(i[code])), ops.map(lambda v: button if v else 0)) # noinspection PyTypeChecker self.buttons = rx.combine_latest( *[value_for(b) for b in MouseButton]).pipe( ops.map(lambda v: reduce(lambda a, b: a | b, v)), ops.distinct_until_changed(), ops.share()) @property def on_mouse_wheel(self) -> Observable: def on_wheel(code: int) -> Observable: return self._activeInputs.pipe( ops.filter(lambda i: code in i), ops.map(lambda i: i[code].values[-1]), ops.filter(lambda v: v != 0)) return rx.merge(on_wheel(bge.events.WHEELUPMOUSE), on_wheel(bge.events.WHEELDOWNMOUSE)) def process(self) -> None: self._position.on_next(mouse.position) self._activeInputs.on_next(mouse.activeInputs) def dispose(self) -> None: super().dispose() self.execute_safely(self._position.dispose) self.execute_safely(self._activeInputs.dispose)