├── .babelrc
├── .eslintrc
├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── src
├── App.jsx
├── react-gtk
│ ├── GtkComponent.js
│ ├── GtkEnvironment.js
│ ├── GtkReconcileTransaction.js
│ ├── components
│ │ ├── Box.js
│ │ ├── Button.js
│ │ ├── Entry.js
│ │ ├── Label.js
│ │ ├── ListBox.js
│ │ ├── ListBoxRow.js
│ │ ├── ScrolledWindow.js
│ │ ├── Spinner.js
│ │ └── TextView.js
│ └── index.js
└── style.css
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "plugins": ["transform-runtime"]
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["airbnb"],
3 | "plugins": ["gjs"],
4 | "env": {
5 | "browser": true,
6 | "node": true,
7 | "es6": true,
8 | "gjs/girepository": true,
9 | "gjs/application": true
10 | },
11 | "rules": {
12 | "import/no-extraneous-dependencies": [2, { "devDependencies": true }],
13 | "semi": [2, "never"],
14 | "space-before-function-paren": [2, "always"],
15 | "no-underscore-dangle": [0],
16 | "class-methods-use-this": [0],
17 | "comma-dangle": [
18 | 'error',
19 | {
20 | arrays: 'always-multiline',
21 | objects: 'always-multiline',
22 | imports: 'always-multiline',
23 | exports: 'always-multiline',
24 | functions: 'never',
25 | },
26 | ],
27 | },
28 | }
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /app.js
3 | /app.js.map
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2017 Derek W. Stavis
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React GTK Experiment
2 |
3 | This is an experimental project for rendering native GTK components.
4 |
5 | It works by running React inside `gjs`, the native GTK JavaScript engine.
6 |
7 | This is highly experimental and should not be used for production :)
8 |
9 | ## Running
10 |
11 | To run the project:
12 |
13 | ```sh
14 | $ yarn
15 | $ yarn start
16 | ```
17 |
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "start": "gjs app.js",
7 | "prestart": "npm run build_dev",
8 | "build": "webpack -p",
9 | "build_dev": "webpack -d",
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "author": "",
13 | "license": "MIT",
14 | "dependencies": {
15 | "babel-runtime": "6.9.2",
16 | "react": "15.2.1"
17 | },
18 | "devDependencies": {
19 | "babel-core": "6.10.4",
20 | "babel-loader": "6.2.4",
21 | "babel-plugin-transform-runtime": "6.9.0",
22 | "babel-preset-es2015": "6.9.0",
23 | "babel-preset-react": "6.11.1",
24 | "eslint": "3.19.0",
25 | "eslint-config-airbnb": "15.0.2",
26 | "eslint-plugin-gjs": "1.0.4",
27 | "eslint-plugin-import": "2.7.0",
28 | "eslint-plugin-jsx-a11y": "5.1.1",
29 | "eslint-plugin-react": "7.1.0",
30 | "text-loader": "^0.0.1",
31 | "webpack": "1.13.1"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import render, {
4 | Box,
5 | Label,
6 | ListBox,
7 | ListBoxRow,
8 | ScrolledWindow,
9 | Spinner,
10 | } from './react-gtk'
11 |
12 | import style from './style.css'
13 |
14 | const Gtk = imports.gi.Gtk
15 | const Gdk = imports.gi.Gdk
16 | const Soup = imports.gi.Soup
17 |
18 | const uriFromRepo = repo =>
19 | new Soup.URI(`https://api.github.com/repos/${repo}/milestones`)
20 |
21 |
22 | class DemoWindow extends React.Component {
23 | constructor (props) {
24 | super(props)
25 |
26 | this.state = {
27 | loading: true,
28 | milestones: [],
29 | repository: 'facebook/react',
30 | }
31 |
32 | this.handleSourceChange = this.handleSourceChange.bind(this)
33 | }
34 |
35 | componentDidMount () {
36 | const uri = uriFromRepo(this.state.repository)
37 | const session = new Soup.SessionAsync()
38 | const request = new Soup.Message({
39 | method: 'GET',
40 | uri,
41 | })
42 |
43 | session.queue_message(
44 | request,
45 | this.handleMilestonesResponse.bind(this, request)
46 | )
47 |
48 | request.request_headers.append('User-Agent', 'GTK Application')
49 | }
50 |
51 | handleMilestonesResponse (request) {
52 | const milestones = JSON.parse(request.response_body.data)
53 |
54 | if (Array.isArray(milestones)) {
55 | this.setState({ milestones, loading: false })
56 | }
57 | }
58 |
59 | handleSourceChange (input) {
60 | print(input)
61 | }
62 |
63 | render () {
64 | return (
65 |
70 | {this.state.loading
71 | ?
72 | : (
73 |
74 |
75 | {this.state.milestones.map(milestone => (
76 |
77 |
81 |
82 | ))}
83 |
84 |
85 | )
86 | }
87 |
88 | )
89 | }
90 | }
91 |
92 | Gtk.init(null, null)
93 |
94 | const window = new Gtk.Window({
95 | width_request: 550,
96 | height_request: 450,
97 | })
98 |
99 | window.set_titlebar(new Gtk.HeaderBar({
100 | title: 'Milestones',
101 | show_close_button: true,
102 | }))
103 |
104 | window.connect('delete-event', () => {
105 | Gtk.main_quit()
106 | return true
107 | })
108 |
109 | const provider = new Gtk.CssProvider()
110 | provider.load_from_data(style)
111 |
112 | Gtk.StyleContext.add_provider_for_screen(
113 | Gdk.Screen.get_default(), provider, 1
114 | )
115 |
116 | render(, window)
117 |
118 |
--------------------------------------------------------------------------------
/src/react-gtk/GtkComponent.js:
--------------------------------------------------------------------------------
1 | import ReactMultiChild from 'react/lib/ReactMultiChild'
2 |
3 | const Gtk = imports.gi.Gtk
4 |
5 | class GtkComponent {
6 | constructor (element) {
7 | this.node = null
8 | this._mountImage = null
9 | this._renderedChildren = null
10 | this._currentElement = element
11 | this._signals = {}
12 | }
13 |
14 | getPublicInstance () {
15 | return this.node
16 | }
17 |
18 | mountComponent (transaction, nativeParent, nativeContainerInfo, context) {
19 | if (!this.node) {
20 | this.node = this.createNewNode()
21 | }
22 |
23 | Object.keys(this._currentElement.props).forEach((prop) => {
24 | this.setProp(prop, this._currentElement.props[prop])
25 | })
26 |
27 | const mountImages = this.mountChildren(
28 | this._currentElement.props.children || null,
29 | transaction,
30 | context
31 | )
32 |
33 | mountImages.forEach((ch) => {
34 | this.node.add(ch)
35 | })
36 |
37 | return this.node
38 | }
39 |
40 | receiveComponent (nextElement, transaction, context) {
41 | const prevElement = this._currentElement
42 | this._currentElement = nextElement
43 |
44 | ;[
45 | ...new Set([
46 | ...Object.keys(prevElement),
47 | ...Object.keys(nextElement.props),
48 | ]),
49 | ].forEach((prop) => {
50 | const prev = prevElement.props[prop]
51 | const next = nextElement.props[prop]
52 | if (prev !== next) {
53 | this.setProp(prop, next)
54 | }
55 | })
56 |
57 | // this.updateChildren comes from ReactMultiChild.Mixin
58 | this.updateChildren(nextElement.props.children, transaction, context)
59 | }
60 |
61 | unmountComponent () {
62 | this.node.get_parent().remove(this.node)
63 | }
64 |
65 | getHostNode () {
66 | return this.node
67 | }
68 |
69 | setProp (prop, value) {
70 | if (prop === 'stylesheet') {
71 | const context = this.node.get_style_context()
72 | const provider = new Gtk.CssProvider()
73 |
74 | provider.load_from_data(value)
75 | context.add_provider(provider, Gtk.CssProvider.PRIORITY_USER)
76 | } else if (prop === 'className') {
77 | const classNames = value.split(' ')
78 | const context = this.node.get_style_context()
79 |
80 | classNames.forEach(className =>
81 | context.add_class(className))
82 | } else if (prop === 'children') {
83 | // child props
84 | } else if (/^on_/.test(prop)) {
85 | const signal = prop.substr(3)
86 |
87 | if (this._signals[signal]) {
88 | this.node.disconnect(this._signals[signal])
89 | }
90 |
91 | this._signals[signal] = this.node.connect(signal, value)
92 | } else if (/__/.test(prop)) {
93 | // parent props
94 | } else {
95 | const cv = this.node[prop]
96 |
97 | if (cv !== value) {
98 | this.node.set_property(prop, value)
99 | }
100 | }
101 | }
102 | }
103 |
104 | Object.assign(
105 | GtkComponent.prototype,
106 | ReactMultiChild.Mixin
107 | )
108 |
109 | export default GtkComponent
110 |
111 |
--------------------------------------------------------------------------------
/src/react-gtk/GtkEnvironment.js:
--------------------------------------------------------------------------------
1 | import ReactComponentEnvironment from 'react/lib/ReactComponentEnvironment'
2 |
3 | const GtkEnvironment = {
4 | processChildrenUpdates (parent, updates) {
5 | updates.forEach(({ type, content }) => {
6 | switch (type) {
7 | case 'INSERT_MARKUP':
8 | parent.node.add(content)
9 | content.show_all()
10 | break
11 | default:
12 | break
13 | }
14 | })
15 | },
16 | }
17 |
18 | export default Object.assign(
19 | ReactComponentEnvironment,
20 | GtkEnvironment
21 | )
22 |
23 |
--------------------------------------------------------------------------------
/src/react-gtk/GtkReconcileTransaction.js:
--------------------------------------------------------------------------------
1 | import CallbackQueue from 'react/lib/CallbackQueue'
2 | import PooledClass from 'react/lib/PooledClass'
3 | import Transaction from 'react/lib/Transaction'
4 | import ReactUpdateQueue from 'react/lib/ReactUpdateQueue'
5 |
6 | /**
7 | * Provides a `CallbackQueue` queue for collecting `onDOMReady` or analogous
8 | * callbacks during the performing of the transaction.
9 | */
10 | const ON_RENDERER_READY_QUEUEING = {
11 | /**
12 | * Initializes the internal firmata `connected` queue.
13 | */
14 | initialize () {
15 | this.reactMountReady.reset()
16 | },
17 |
18 | /**
19 | * After Hardware is connected, invoke all registered `ready` callbacks.
20 | */
21 | close () {
22 | this.reactMountReady.notifyAll()
23 | },
24 | }
25 |
26 | /**
27 | * Executed within the scope of the `Transaction` instance. Consider these as
28 | * being member methods, but with an implied ordering while being isolated from
29 | * each other.
30 | */
31 | const TRANSACTION_WRAPPERS = [ON_RENDERER_READY_QUEUEING]
32 |
33 | function GtkReconcileTransaction () {
34 | this.reinitializeTransaction()
35 | this.reactMountReady = CallbackQueue.getPooled(null)
36 | }
37 |
38 | const Mixin = {
39 | /**
40 | * @see Transaction
41 | * @abstract
42 | * @final
43 | * @return {array