Repository: electron
Issue: [FeatureRequest]: native ScrollView in the main process
Labels: enhancement :sparkles:

# Problem

Hey Electron Community 👋

We're developing a tab-less browser that allows users to open sites in cards and align them as a grid on an infinite canvas.

Additionally, we intend to create a panoramic scrolling functionality, which allows users to navigate between cards by swiping left and right

The following GIF illustrates a simplified version of the layout and the horizontal scrolling experience:

![150981006-84fe796e-60bd-43c0-952d-831d1bfd9998.gif](https://user-images.githubusercontent.com/2891206/150981006-84fe796e-60bd-43c0-952d-831d1bfd9998.gif)

We use BrowserViews for showing the content of the website.

- For creating layouts we combined `BrowserView.setBounds` API together with [Facebook YOGA](https://yogalayout.com/playground).
- **[Problem]:** We want to create scroll functionality. It consists of 2 parts:
   - Capture mouse events
   - Move BrowserViews accordingly left and right, or up and down.

## Existing Solution

To capture mouse events we inject custom scripts in the webContents.

It listens to the wheel event on the document and sends `deltaX` and `deltaY` to the main process.

On the other side, the main process receives delta value and recalculates the bounds of each BrowserView

```javascript
// injected script

document.on('wheel', (e) => {
  ipcRenderer.invoke('scroll', e.deltaX)
}, { passive: true })


// main

const views = [ /* collection of browserviews */]

ipcMain.handle('scroll', (e, deltaX) => {
  views.forEach(view => {
    const { x } = view.getBounds()
    view.setBounds({ x: x + deltaX })
  })
})
```

This approach does not work because of the following reasons.

- Performance is not smooth enough, especially it suffers on Windows.
- *(for trackpad users)* while scrolling whenever the cursor jumps from one BrowserView to another, the document stops to receive wheel events and it gets stuck at the edge. You need to lift the finger from the trackpad and trigger the scroll again. There's no consistency, but it does happen very often. Check out the Demo

### Demo

[![image](https://user-images.githubusercontent.com/2891206/152532346-f14047d1-35bd-4622-b2e9-c804755b4dad.png)](https://www.youtube.com/watch?v=X9HoOMy8wHQ)

The following gist for the electron fiddle also demonstrates the same problem: https://gist.github.com/ddramone/305d9f8fe1a02851beecb407e8b876fb

## Proposal

We would like to propose adding an experimental `ScrollView` and `ContainerView` APIs in the main process.

```javascript
//main

const { BaseWindow, ContainerView, ScrollView, BrowserView } = require("electron");

const window = new BaseWindow({ width: 1400, height: 700 })

const scroll = new ScrollView()
scroll.setBounds({ width: 1400, height: 700, x: 0, y: 0 })

const scrollContent = new ContainerView()

let CONTENT_WIDTH = 0


[ /* ... list of cards ... */ ].forEach((card) => {

  const { url, bounds } = card

  const browserView = new BrowserView()

  browserView.webContents.loadURL(url)
  browserView.setBounds(bounds)
  
  scrollContent.addBrowserView(browserView)

  CONTENT_WIDTH += bounds.width

});

scrollContent.setBounds({ width: CONTENT_WIDTH, height: 700, x: 0, y: 0 })

scroll.setContentView(scrollContent);
win.setContainerView(scroll);
```

This approach allows us to create a scrollable content container by using `ContainerView` and adding `BrowserViews` as its children.

Then we add `ContainerView` as a direct child of the `ScrollView` which is responsible to capture mouse events and scroll the content accordingly.

### How:

In order to achieve the presented assumptions, we have implemented an API that allows building a hierarchy of various types of views. To improve the clarity of the API, We defined `ContainerView` and `ScrollView` which inherit from `BaseView`.

We tried to keep the consistent implementation across all platforms, which is compatible with the Electron architecture - On MacOS we used `NSView` to support draggable regions, on Windows and Linux we used Chromium's `views :: View` 

We have this approach tested with positive results on both platforms.

Currently, @martin-world1977 is in the process of finalizing the API, testing, and cleaning up changes.

He's the person working on electron full-time.

*While we’re preparing pull request, it would be great to hear your opinion.*


----

#### Shortly About US:

We are Stack and we are building a browser that has focus, organization & collaboration at its core.

Why? Because 2.5b people every day are using a tool (conventional browsers) design of which hasn't changed for more than 20 years. Tab clutter slows us down and we believe the time has come to change that.

We already have a working product that is built using Electron `<webview />` and is used by more than 15K people on daily basis. A few months ago, we came across unsolvable technical challenges and decided to rewrite Stack from scratch. After experimenting with various native technologies, we settled back to Electron by using BrowserViews instead of WebViews.

We are a young, growing company with a team of 20 based in Amsterdam, The Netherlands.  (www.stackbrowser.com)

We're excited to contribute to the Electron ecosystem and make it even more powerful.